From e4ccf19f758ce03646ca2d0fb91c7de60d5fc52a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 6 Jan 2020 16:54:37 +0100 Subject: [PATCH 001/137] [Uptime] Fix/location map hide layer view control (#53568) * hide layer control and added loc tags * update test * remove unused comment * remove capitalization * style fix * update types Co-authored-by: Elastic Machine --- .../{ => __tests__}/__mocks__/mock.ts | 2 +- .../{ => __tests__}/map_config.test.ts | 6 +- .../location_map/embeddables/embedded_map.tsx | 14 ++-- .../location_map/embeddables/map_config.ts | 12 ++- .../functional/location_map/location_map.tsx | 21 ++++-- .../location_map/location_status_tags.tsx | 73 +++++++++++++++++++ 6 files changed, 110 insertions(+), 18 deletions(-) rename x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/{ => __tests__}/__mocks__/mock.ts (98%) rename x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/{ => __tests__}/map_config.test.ts (83%) create mode 100644 x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/__mocks__/mock.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__mocks__/mock.ts rename to x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/__mocks__/mock.ts index 9b902651690bf0..291ab555fbdc67 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/__mocks__/mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import lowPolyLayerFeatures from '../low_poly_layer.json'; +import lowPolyLayerFeatures from '../../low_poly_layer.json'; export const mockDownPointsLayer = { id: 'down_points', diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.test.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts similarity index 83% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.test.ts rename to x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts index 1e8e5b6012a79d..263c3fc787da91 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.test.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__tests__/map_config.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getLayerList } from './map_config'; +import { getLayerList } from '../map_config'; import { mockLayerList } from './__mocks__/mock'; -import { LocationPoint } from './embedded_map'; +import { LocationPoint } from '../embedded_map'; jest.mock('uuid', () => { return { @@ -33,7 +33,7 @@ describe('map_config', () => { describe('#getLayerList', () => { test('it returns the low poly layer', () => { - const layerList = getLayerList(upPoints, downPoints); + const layerList = getLayerList(upPoints, downPoints, { danger: '#BC261E', gray: '#000' }); expect(layerList).toStrictEqual(mockLayerList); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx index 93de1d478fb836..fe8a1a0bad7ec6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; @@ -15,6 +15,7 @@ import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; import { MapEmbeddable } from './types'; import { getLayerList } from './map_config'; +import { UptimeSettingsContext } from '../../../../contexts'; export interface EmbeddedMapProps { upPoints: LocationPoint[]; @@ -45,6 +46,7 @@ const EmbeddedPanel = styled.div` `; export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => { + const { colors } = useContext(UptimeSettingsContext); const [embeddable, setEmbeddable] = useState(); const embeddableRoot: React.RefObject = React.createRef(); const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); @@ -58,16 +60,18 @@ export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => { viewMode: 'view', isLayerTOCOpen: false, hideFilterActions: true, - mapCenter: { lon: 11, lat: 47, zoom: 0 }, + mapCenter: { lon: 11, lat: 20, zoom: 0 }, disableInteractive: true, disableTooltipControl: true, hideToolbarOverlay: true, + hideLayerControl: true, + hideViewControl: true, }; useEffect(() => { async function setupEmbeddable() { const mapState = { - layerList: getLayerList(upPoints, downPoints), + layerList: getLayerList(upPoints, downPoints, colors), title: i18n.MAP_TITLE, }; // @ts-ignore @@ -82,9 +86,9 @@ export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => { useEffect(() => { if (embeddable) { - embeddable.setLayerList(getLayerList(upPoints, downPoints)); + embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); } - }, [upPoints, downPoints, embeddable]); + }, [upPoints, downPoints, embeddable, colors]); useEffect(() => { if (embeddableRoot.current && embeddable) { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts index 608df8b235f001..b423b8baf41bf6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts @@ -12,8 +12,12 @@ import { LocationPoint } from './embedded_map'; * destination, and line layer for each of the provided indexPatterns * */ -export const getLayerList = (upPoints: LocationPoint[], downPoints: LocationPoint[]) => { - return [getLowPolyLayer(), getDownPointsLayer(downPoints), getUpPointsLayer(upPoints)]; +export const getLayerList = ( + upPoints: LocationPoint[], + downPoints: LocationPoint[], + { gray, danger }: { gray: string; danger: string } +) => { + return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)]; }; export const getLowPolyLayer = () => { @@ -62,7 +66,7 @@ export const getLowPolyLayer = () => { }; }; -export const getDownPointsLayer = (downPoints: LocationPoint[]) => { +export const getDownPointsLayer = (downPoints: LocationPoint[], dangerColor: string) => { const features = downPoints?.map(point => ({ type: 'feature', geometry: { @@ -87,7 +91,7 @@ export const getDownPointsLayer = (downPoints: LocationPoint[]) => { fillColor: { type: 'STATIC', options: { - color: '#BC261E', + color: dangerColor, }, }, lineColor: { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx index b271632cb631fd..f70d145ec05c38 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx @@ -6,15 +6,19 @@ import React from 'react'; import styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { LocationStatusTags } from './location_status_tags'; import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; +import { MonitorLocations } from '../../../../common/runtime_types'; const MapPanel = styled.div` - height: 400px; + height: 240px; width: 520px; + margin-right: 10px; `; interface LocationMapProps { - monitorLocations: any; + monitorLocations: MonitorLocations; } export const LocationMap = ({ monitorLocations }: LocationMapProps) => { @@ -31,8 +35,15 @@ export const LocationMap = ({ monitorLocations }: LocationMapProps) => { }); } return ( - - - + + + + + + + + + + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx new file mode 100644 index 00000000000000..a10d8e02e68638 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import styled from 'styled-components'; +import { EuiBadge, EuiText } from '@elastic/eui'; +import { UptimeSettingsContext } from '../../../contexts'; +import { MonitorLocation } from '../../../../common/runtime_types'; + +const TextStyle = styled.div` + font-weight: 600; +`; + +const BadgeItem = styled.div` + margin-bottom: 5px; +`; + +const TagContainer = styled.div` + padding: 10px; + max-height: 200px; + overflow: hidden; +`; + +interface Props { + locations: MonitorLocation[]; +} + +export const LocationStatusTags = ({ locations }: Props) => { + const { + colors: { gray, danger }, + } = useContext(UptimeSettingsContext); + + const upLocs: string[] = []; + const downLocs: string[] = []; + + locations.forEach((item: any) => { + if (item.summary.down === 0) { + upLocs.push(item.geo.name); + } else { + downLocs.push(item.geo.name); + } + }); + + return ( + + + {downLocs.map((item, ind) => ( + + + + {item} + + + + ))} + + + {upLocs.map((item, ind) => ( + + + + {item} + + + + ))} + + + ); +}; From 1e5135ec538e2fa5fcd84ca463d9aa63320ada4b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 6 Jan 2020 11:42:01 -0500 Subject: [PATCH 002/137] [Maps] Vector style UI redesign (#53946) * [Maps] style editor update * update label editor * update size editor * update orienation editor * i18n cleanup * deconstruct props * review feedback Co-authored-by: Elastic Machine --- .../components/color/dynamic_color_form.js | 62 ++++++++ .../color/dynamic_color_selection.js | 48 ------ .../components/color/static_color_form.js | 33 ++++ .../color/static_color_selection.js | 30 ---- .../color/vector_style_color_editor.js | 34 ++-- .../components/label/dynamic_label_form.js | 37 +++++ .../label/dynamic_label_selector.js | 24 --- .../components/label/static_label_form.js | 34 ++++ .../components/label/static_label_selector.js | 28 ---- .../label/vector_style_label_editor.js | 18 +-- .../orientation/dynamic_orientation_form.js | 40 +++++ .../dynamic_orientation_selection.js | 32 ---- .../orientation/orientation_editor.js | 22 ++- .../orientation/static_orientation_form.js | 33 ++++ .../static_orientation_selection.js | 34 ---- .../components/size/dynamic_size_form.js | 63 ++++++++ .../components/size/dynamic_size_selection.js | 48 ------ .../components/size/static_size_form.js | 37 +++++ .../components/size/static_size_selection.js | 38 ----- .../size/vector_style_size_editor.js | 22 ++- .../components/static_dynamic_style_row.js | 145 ------------------ .../vector/components/style_prop_editor.js | 104 +++++++++++++ .../vector/components/vector_style_editor.js | 127 +++++++++++---- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 25 files changed, 585 insertions(+), 512 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js new file mode 100644 index 00000000000000..5e0f7434b04d08 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -0,0 +1,62 @@ +/* + * 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 _ from 'lodash'; +import React, { Fragment } from 'react'; +import { FieldSelect } from '../field_select'; +import { ColorRampSelect } from './color_ramp_select'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +export function DynamicColorForm({ + fields, + onDynamicStyleChange, + staticDynamicSelect, + styleProperty, +}) { + const styleOptions = styleProperty.getOptions(); + + const onFieldChange = ({ field }) => { + onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field }); + }; + + const onColorChange = colorOptions => { + onDynamicStyleChange(styleProperty.getStyleName(), { + ...styleOptions, + ...colorOptions, + }); + }; + + let colorRampSelect; + if (styleOptions.field && styleOptions.field.name) { + colorRampSelect = ( + + ); + } + + return ( + + + {staticDynamicSelect} + + + + + + {colorRampSelect} + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js deleted file mode 100644 index 84327635f2b65a..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js +++ /dev/null @@ -1,48 +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 _ from 'lodash'; -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { dynamicColorShape } from '../style_option_shapes'; -import { FieldSelect, fieldShape } from '../field_select'; -import { ColorRampSelect } from './color_ramp_select'; -import { EuiSpacer } from '@elastic/eui'; - -export function DynamicColorSelection({ fields, onChange, styleOptions }) { - const onFieldChange = ({ field }) => { - onChange({ ...styleOptions, field }); - }; - - const onColorChange = colorOptions => { - onChange({ ...styleOptions, ...colorOptions }); - }; - - return ( - - - - - - ); -} - -DynamicColorSelection.propTypes = { - fields: PropTypes.arrayOf(fieldShape).isRequired, - styleOptions: dynamicColorShape.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js new file mode 100644 index 00000000000000..48befa1ca74c0e --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js @@ -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 React from 'react'; +import { EuiColorPicker, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export function StaticColorForm({ + onStaticStyleChange, + staticDynamicSelect, + styleProperty, + swatches, +}) { + const onColorChange = color => { + onStaticStyleChange(styleProperty.getStyleName(), { color }); + }; + + return ( + + {staticDynamicSelect} + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js deleted file mode 100644 index e42b582dc39297..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js +++ /dev/null @@ -1,30 +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 { EuiColorPicker } from '@elastic/eui'; -import { staticColorShape } from '../style_option_shapes'; - -export function StaticColorSelection({ onChange, styleOptions, swatches }) { - const onColorChange = color => { - onChange({ color }); - }; - - return ( - - ); -} - -StaticColorSelection.propTypes = { - styleOptions: staticColorShape.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js index c7745fa69a82f8..43e7050b3d1d23 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js @@ -6,21 +6,29 @@ import React from 'react'; -import { StaticDynamicStyleRow } from '../static_dynamic_style_row'; -import { DynamicColorSelection } from './dynamic_color_selection'; -import { StaticColorSelection } from './static_color_selection'; +import { StylePropEditor } from '../style_prop_editor'; +import { DynamicColorForm } from './dynamic_color_form'; +import { StaticColorForm } from './static_color_form'; +import { i18n } from '@kbn/i18n'; export function VectorStyleColorEditor(props) { + const colorForm = props.styleProperty.isDynamic() ? ( + + ) : ( + + ); + return ( - + + {colorForm} + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js new file mode 100644 index 00000000000000..bad13b487cc295 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js @@ -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 _ from 'lodash'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FieldSelect } from '../field_select'; + +export function DynamicLabelForm({ + fields, + onDynamicStyleChange, + staticDynamicSelect, + styleProperty, +}) { + const styleOptions = styleProperty.getOptions(); + + const onFieldChange = ({ field }) => { + onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field }); + }; + + return ( + + {staticDynamicSelect} + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js deleted file mode 100644 index e393341b906960..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import React from 'react'; -import { FieldSelect } from '../field_select'; - -export function DynamicLabelSelector({ fields, styleOptions, onChange }) { - const onFieldChange = ({ field }) => { - onChange({ ...styleOptions, field }); - }; - - return ( - - ); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js new file mode 100644 index 00000000000000..721487b5d8ff09 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js @@ -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 { i18n } from '@kbn/i18n'; +import { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export function StaticLabelForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) { + const onValueChange = event => { + onStaticStyleChange(styleProperty.getStyleName(), { value: event.target.value }); + }; + + return ( + + {staticDynamicSelect} + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js deleted file mode 100644 index ea296a3312799e..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFieldText } from '@elastic/eui'; - -export function StaticLabelSelector({ onChange, styleOptions }) { - const onValueChange = event => { - onChange({ value: event.target.value }); - }; - - return ( - - ); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js index 6bca56425d38d5..aaa21ea315f36c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js @@ -6,16 +6,16 @@ import React from 'react'; -import { StaticDynamicStyleRow } from '../static_dynamic_style_row'; -import { DynamicLabelSelector } from './dynamic_label_selector'; -import { StaticLabelSelector } from './static_label_selector'; +import { StylePropEditor } from '../style_prop_editor'; +import { DynamicLabelForm } from './dynamic_label_form'; +import { StaticLabelForm } from './static_label_form'; export function VectorStyleLabelEditor(props) { - return ( - + const labelForm = props.styleProperty.isDynamic() ? ( + + ) : ( + ); + + return {labelForm}; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js new file mode 100644 index 00000000000000..e0b7e7b2865a27 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js @@ -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 _ from 'lodash'; +import React from 'react'; +import { FieldSelect } from '../field_select'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export function DynamicOrientationForm({ + fields, + onDynamicStyleChange, + staticDynamicSelect, + styleProperty, +}) { + const styleOptions = styleProperty.getOptions(); + + const onFieldChange = ({ field }) => { + onDynamicStyleChange(styleProperty.getStyleName(), { + ...styleOptions, + field, + }); + }; + + return ( + + {staticDynamicSelect} + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js deleted file mode 100644 index 8ad3916ac65093..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import React from 'react'; -import PropTypes from 'prop-types'; -import { dynamicOrientationShape } from '../style_option_shapes'; -import { FieldSelect, fieldShape } from '../field_select'; - -export function DynamicOrientationSelection({ fields, styleOptions, onChange }) { - const onFieldChange = ({ field }) => { - onChange({ ...styleOptions, field }); - }; - - return ( - - ); -} - -DynamicOrientationSelection.propTypes = { - fields: PropTypes.arrayOf(fieldShape).isRequired, - styleOptions: dynamicOrientationShape.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js index e97252a5e79da4..915fc92c9fb381 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js @@ -6,20 +6,16 @@ import React from 'react'; -import { StaticDynamicStyleRow } from '../static_dynamic_style_row'; -import { DynamicOrientationSelection } from './dynamic_orientation_selection'; -import { StaticOrientationSelection } from './static_orientation_selection'; +import { StylePropEditor } from '../style_prop_editor'; +import { DynamicOrientationForm } from './dynamic_orientation_form'; +import { StaticOrientationForm } from './static_orientation_form'; export function OrientationEditor(props) { - return ( - + const orientationForm = props.styleProperty.isDynamic() ? ( + + ) : ( + ); + + return {orientationForm}; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js new file mode 100644 index 00000000000000..8c4418f95e1d25 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js @@ -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 React from 'react'; +import { ValidatedRange } from '../../../../../components/validated_range'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export function StaticOrientationForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) { + const onOrientationChange = orientation => { + onStaticStyleChange(styleProperty.getStyleName(), { orientation }); + }; + + return ( + + {staticDynamicSelect} + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.js deleted file mode 100644 index b5529c69874598..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.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 React from 'react'; -import PropTypes from 'prop-types'; -import { staticOrientationShape } from '../style_option_shapes'; -import { ValidatedRange } from '../../../../../components/validated_range'; - -export function StaticOrientationSelection({ onChange, styleOptions }) { - const onOrientationChange = orientation => { - onChange({ orientation }); - }; - - return ( - - ); -} - -StaticOrientationSelection.propTypes = { - styleOptions: staticOrientationShape.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js new file mode 100644 index 00000000000000..8b069cd53b731a --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js @@ -0,0 +1,63 @@ +/* + * 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 _ from 'lodash'; +import React, { Fragment } from 'react'; +import { FieldSelect } from '../field_select'; +import { SizeRangeSelector } from './size_range_selector'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +export function DynamicSizeForm({ + fields, + onDynamicStyleChange, + staticDynamicSelect, + styleProperty, +}) { + const styleOptions = styleProperty.getOptions(); + + const onFieldChange = ({ field }) => { + onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field }); + }; + + const onSizeRangeChange = ({ minSize, maxSize }) => { + onDynamicStyleChange(styleProperty.getStyleName(), { + ...styleOptions, + minSize, + maxSize, + }); + }; + + let sizeRange; + if (styleOptions.field && styleOptions.field.name) { + sizeRange = ( + + ); + } + + return ( + + + {staticDynamicSelect} + + + + + + {sizeRange} + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js deleted file mode 100644 index 76c5b97976bbcd..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js +++ /dev/null @@ -1,48 +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 _ from 'lodash'; -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { dynamicSizeShape } from '../style_option_shapes'; -import { FieldSelect, fieldShape } from '../field_select'; -import { SizeRangeSelector } from './size_range_selector'; -import { EuiSpacer } from '@elastic/eui'; - -export function DynamicSizeSelection({ fields, styleOptions, onChange }) { - const onFieldChange = ({ field }) => { - onChange({ ...styleOptions, field }); - }; - - const onSizeRangeChange = ({ minSize, maxSize }) => { - onChange({ ...styleOptions, minSize, maxSize }); - }; - - return ( - - - - - - ); -} - -DynamicSizeSelection.propTypes = { - fields: PropTypes.arrayOf(fieldShape).isRequired, - styleOptions: dynamicSizeShape.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js new file mode 100644 index 00000000000000..d8fe1322db3e36 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js @@ -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 { ValidatedRange } from '../../../../../components/validated_range'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export function StaticSizeForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) { + const onSizeChange = size => { + onStaticStyleChange(styleProperty.getStyleName(), { size }); + }; + + return ( + + {staticDynamicSelect} + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js deleted file mode 100644 index 38f8fe53d17488..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js +++ /dev/null @@ -1,38 +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 { staticSizeShape } from '../style_option_shapes'; -import { ValidatedRange } from '../../../../../components/validated_range'; -import { i18n } from '@kbn/i18n'; - -export function StaticSizeSelection({ onChange, styleOptions }) { - const onSizeChange = size => { - onChange({ size }); - }; - - return ( - - ); -} - -StaticSizeSelection.propTypes = { - styleOptions: staticSizeShape.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js index 6580bfc00e0ad9..e344f72bd429ad 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js @@ -6,20 +6,16 @@ import React from 'react'; -import { StaticDynamicStyleRow } from '../static_dynamic_style_row'; -import { DynamicSizeSelection } from './dynamic_size_selection'; -import { StaticSizeSelection } from './static_size_selection'; +import { StylePropEditor } from '../style_prop_editor'; +import { DynamicSizeForm } from './dynamic_size_form'; +import { StaticSizeForm } from './static_size_form'; export function VectorStyleSizeEditor(props) { - return ( - + const sizeForm = props.styleProperty.isDynamic() ? ( + + ) : ( + ); + + return {sizeForm}; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js deleted file mode 100644 index 311406731801aa..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js +++ /dev/null @@ -1,145 +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, Fragment } from 'react'; -import { VectorStyle } from '../vector_style'; -import { i18n } from '@kbn/i18n'; -import { FieldMetaOptionsPopover } from './field_meta_options_popover'; -import { getVectorStyleLabel } from './get_vector_style_label'; - -import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiFormRow, EuiButtonToggle } from '@elastic/eui'; - -export class StaticDynamicStyleRow extends Component { - // Store previous options locally so when type is toggled, - // previous style options can be used. - prevStaticStyleOptions = this.props.defaultStaticStyleOptions; - prevDynamicStyleOptions = this.props.defaultDynamicStyleOptions; - - _canBeDynamic() { - return this.props.fields.length > 0; - } - - _isDynamic() { - return this.props.styleProperty.isDynamic(); - } - - _getStyleOptions() { - return this.props.styleProperty.getOptions(); - } - - _onFieldMetaOptionsChange = fieldMetaOptions => { - const styleDescriptor = { - type: VectorStyle.STYLE_TYPE.DYNAMIC, - options: { - ...this._getStyleOptions(), - fieldMetaOptions, - }, - }; - this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor); - }; - - _onStaticStyleChange = options => { - const styleDescriptor = { - type: VectorStyle.STYLE_TYPE.STATIC, - options, - }; - this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor); - }; - - _onDynamicStyleChange = options => { - const styleDescriptor = { - type: VectorStyle.STYLE_TYPE.DYNAMIC, - options, - }; - this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor); - }; - - _onTypeToggle = () => { - if (this._isDynamic()) { - // preserve current dynmaic style - this.prevDynamicStyleOptions = this._getStyleOptions(); - // toggle to static style - this._onStaticStyleChange(this.prevStaticStyleOptions); - return; - } - - // preserve current static style - this.prevStaticStyleOptions = this._getStyleOptions(); - // toggle to dynamic style - this._onDynamicStyleChange(this.prevDynamicStyleOptions); - }; - - _renderStyleSelector() { - if (this._isDynamic()) { - const DynamicSelector = this.props.DynamicSelector; - return ( - - - - - ); - } - - const StaticSelector = this.props.StaticSelector; - return ( - - ); - } - - render() { - const isDynamic = this._isDynamic(); - const dynamicTooltipContent = isDynamic - ? i18n.translate('xpack.maps.styles.staticDynamic.staticDescription', { - defaultMessage: 'Use static styling properties to symbolize features.', - }) - : i18n.translate('xpack.maps.styles.staticDynamic.dynamicDescription', { - defaultMessage: 'Use property values to symbolize features.', - }); - - return ( - - - - {this._renderStyleSelector()} - - - {this._canBeDynamic() && ( - - - - - - - - )} - - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js new file mode 100644 index 00000000000000..1ac8edfb2cc694 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js @@ -0,0 +1,104 @@ +/* + * 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, Fragment } from 'react'; +import { FieldMetaOptionsPopover } from './field_meta_options_popover'; +import { getVectorStyleLabel } from './get_vector_style_label'; +import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { VectorStyle } from '../vector_style'; +import { i18n } from '@kbn/i18n'; + +export class StylePropEditor extends Component { + _prevStaticStyleOptions = this.props.defaultStaticStyleOptions; + _prevDynamicStyleOptions = this.props.defaultDynamicStyleOptions; + + _onTypeToggle = () => { + if (this.props.styleProperty.isDynamic()) { + // preserve current dynmaic style + this._prevDynamicStyleOptions = this.props.styleProperty.getOptions(); + // toggle to static style + this.props.onStaticStyleChange( + this.props.styleProperty.getStyleName(), + this._prevStaticStyleOptions + ); + } else { + // preserve current static style + this._prevStaticStyleOptions = this.props.styleProperty.getOptions(); + // toggle to dynamic style + this.props.onDynamicStyleChange( + this.props.styleProperty.getStyleName(), + this._prevDynamicStyleOptions + ); + } + }; + + _onFieldMetaOptionsChange = fieldMetaOptions => { + const options = { + ...this.props.styleProperty.getOptions(), + fieldMetaOptions, + }; + this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), options); + }; + + renderStaticDynamicSelect() { + const options = [ + { + value: VectorStyle.STYLE_TYPE.STATIC, + text: this.props.customStaticOptionLabel + ? this.props.customStaticOptionLabel + : i18n.translate('xpack.maps.styles.staticDynamicSelect.staticLabel', { + defaultMessage: 'Fixed', + }), + }, + { + value: VectorStyle.STYLE_TYPE.DYNAMIC, + text: i18n.translate('xpack.maps.styles.staticDynamicSelect.dynamicLabel', { + defaultMessage: 'By value', + }), + }, + ]; + + return ( + + ); + } + + render() { + const fieldMetaOptionsPopover = this.props.styleProperty.isDynamic() ? ( + + ) : null; + + return ( + + + {React.cloneElement(this.props.children, { + staticDynamicSelect: this.renderStaticDynamicSelect(), + })} + {fieldMetaOptionsPopover} + + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 44f630db9d890e..8e80e036dbb8bc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -12,8 +12,13 @@ import { VectorStyleColorEditor } from './color/vector_style_color_editor'; import { VectorStyleSizeEditor } from './size/vector_style_size_editor'; import { VectorStyleSymbolEditor } from './vector_style_symbol_editor'; import { VectorStyleLabelEditor } from './label/vector_style_label_editor'; +import { VectorStyle } from '../vector_style'; import { OrientationEditor } from './orientation/orientation_editor'; -import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../vector_style_defaults'; +import { + getDefaultDynamicProperties, + getDefaultStaticProperties, + VECTOR_STYLES, +} from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils'; import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types'; import { SYMBOLIZE_AS_ICON } from '../vector_constants'; @@ -128,15 +133,36 @@ export class VectorStyleEditor extends Component { this.props.onIsTimeAwareChange(event.target.checked); }; + _onStaticStyleChange = (propertyName, options) => { + const styleDescriptor = { + type: VectorStyle.STYLE_TYPE.STATIC, + options, + }; + this.props.handlePropertyChange(propertyName, styleDescriptor); + }; + + _onDynamicStyleChange = (propertyName, options) => { + const styleDescriptor = { + type: VectorStyle.STYLE_TYPE.DYNAMIC, + options, + }; + this.props.handlePropertyChange(propertyName, styleDescriptor); + }; + _renderFillColor() { return ( ); } @@ -145,11 +171,16 @@ export class VectorStyleEditor extends Component { return ( ); } @@ -157,11 +188,16 @@ export class VectorStyleEditor extends Component { _renderLineWidth() { return ( ); } @@ -169,11 +205,16 @@ export class VectorStyleEditor extends Component { _renderSymbolSize() { return ( ); } @@ -182,30 +223,45 @@ export class VectorStyleEditor extends Component { return ( @@ -217,11 +273,16 @@ export class VectorStyleEditor extends Component { if (this.props.symbolDescriptor.options.symbolizeAs === SYMBOLIZE_AS_ICON) { iconOrientation = ( ); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 83ef497e50649a..af43110a8ba5e8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6588,8 +6588,6 @@ "xpack.maps.source.wmsTitle": "ウェブマップサービス", "xpack.maps.style.heatmap.displayNameLabel": "ヒートマップスタイル", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "解像度パラメーターが認識されません: {resolution}", - "xpack.maps.styles.staticDynamic.dynamicDescription": "プロパティ値で特徴をシンボル化します。", - "xpack.maps.styles.staticDynamic.staticDescription": "静的スタイルプロパティで特徴をシンボル化します。", "xpack.maps.styles.vector.borderColorLabel": "境界線の色", "xpack.maps.styles.vector.borderWidthLabel": "境界線の幅", "xpack.maps.styles.vector.fillColorLabel": "塗りつぶす色", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 87c11adcb5e777..0306edcabd67d2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6590,8 +6590,6 @@ "xpack.maps.source.wmsTitle": "Web 地图服务", "xpack.maps.style.heatmap.displayNameLabel": "热图样式", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "无法识别分辨率参数:{resolution}", - "xpack.maps.styles.staticDynamic.dynamicDescription": "使用属性值代表功能。", - "xpack.maps.styles.staticDynamic.staticDescription": "使用静态样式属性代表功能。", "xpack.maps.styles.vector.borderColorLabel": "边框颜色", "xpack.maps.styles.vector.borderWidthLabel": "边框宽度", "xpack.maps.styles.vector.fillColorLabel": "填充颜色", From eb5460fe6674c1171f71714c9ade89fb6e649707 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Mon, 6 Jan 2020 10:31:29 -0700 Subject: [PATCH 003/137] [Metrics UI] Add AWS Cloudwatch dimensions to groups filter in Metrics Explorer (#53624) * [Metrics UI] Adding aws.cloudwatch.* fields to group by for aws.metrics in Metrics Explorer * Ensuring the correct module is used for the field. * Adding a unique around the fields to prevent potential extra work * Adding missing file * Fixing possible missing event.dataset * clean up for review --- .../plugins/infra/common/ecs_allowed_list.ts | 10 ++-- .../create_timerange_with_interval.ts | 46 +++++++++++-------- .../lib/get_dataset_for_field.ts | 46 +++++++++++++++++++ .../lib/populate_series_with_tsvb_data.ts | 18 ++++---- 4 files changed, 90 insertions(+), 30 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts diff --git a/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts b/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts index 1728cd1fa4b45d..f1d0577b4cb199 100644 --- a/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts +++ b/x-pack/legacy/plugins/infra/common/ecs_allowed_list.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 { first } from 'lodash'; +import { first, memoize } from 'lodash'; export const ECS_ALLOWED_LIST = [ 'host', @@ -46,8 +46,9 @@ export const DOCKER_ALLOWED_LIST = [ ]; export const AWS_S3_ALLOWED_LIST = ['aws.s3']; +export const AWS_METRICS_ALLOWED_LIST = ['aws.cloudwatch']; -export const getAllowedListForPrefix = (prefix: string) => { +export const getAllowedListForPrefix = memoize((prefix: string) => { const firstPart = first(prefix.split(/\./)); const defaultAllowedList = prefix ? [...ECS_ALLOWED_LIST, prefix] : ECS_ALLOWED_LIST; switch (firstPart) { @@ -61,7 +62,10 @@ export const getAllowedListForPrefix = (prefix: string) => { if (prefix === 'aws.s3_daily_storage') { return [...defaultAllowedList, ...AWS_S3_ALLOWED_LIST]; } + if (prefix === 'aws.metrics') { + return [...defaultAllowedList, ...AWS_METRICS_ALLOWED_LIST]; + } default: return defaultAllowedList; } -}; +}); diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index 6c27e54a78bee4..4bb1aefb5b2c72 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -12,6 +12,7 @@ import { getMetricsAggregations } from './query_helpers'; import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types'; import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; +import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field'; export const createTimeRangeWithInterval = async ( framework: KibanaFramework, @@ -19,7 +20,7 @@ export const createTimeRangeWithInterval = async ( options: InfraSnapshotRequestOptions ): Promise => { const aggregations = getMetricsAggregations(options); - const modules = aggregationsToModules(aggregations); + const modules = await aggregationsToModules(framework, requestContext, aggregations, options); const interval = (await calculateMetricInterval( framework, @@ -39,21 +40,30 @@ export const createTimeRangeWithInterval = async ( }; }; -const aggregationsToModules = (aggregations: SnapshotModel): string[] => { - return uniq( - Object.values(aggregations) - .reduce((modules, agg) => { - if (SnapshotModelMetricAggRT.is(agg)) { - return modules.concat(Object.values(agg).map(a => a?.field)); - } - return modules; - }, [] as Array) - .filter(v => v) - .map(field => - field! - .split(/\./) - .slice(0, 2) - .join('.') - ) - ) as string[]; +const aggregationsToModules = async ( + framework: KibanaFramework, + requestContext: RequestHandlerContext, + aggregations: SnapshotModel, + options: InfraSnapshotRequestOptions +): Promise => { + const uniqueFields = Object.values(aggregations) + .reduce>((fields, agg) => { + if (SnapshotModelMetricAggRT.is(agg)) { + return uniq(fields.concat(Object.values(agg).map(a => a?.field))); + } + return fields; + }, []) + .filter(v => v) as string[]; + const fields = await Promise.all( + uniqueFields.map( + async field => + await getDatasetForField( + framework, + requestContext, + field as string, + options.sourceConfiguration.metricAlias + ) + ) + ); + return fields.filter(f => f) as string[]; }; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts new file mode 100644 index 00000000000000..66f0ca8fc706a3 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'kibana/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; + +interface EventDatasetHit { + _source: { + event?: { + dataset?: string; + }; + }; +} + +export const getDatasetForField = async ( + framework: KibanaFramework, + requestContext: RequestHandlerContext, + field: string, + indexPattern: string +) => { + const params = { + allowNoIndices: true, + ignoreUnavailable: true, + terminateAfter: 1, + index: indexPattern, + body: { + query: { exists: { field } }, + size: 1, + _source: ['event.dataset'], + }, + }; + + const response = await framework.callWithRequest( + requestContext, + 'search', + params + ); + if (response.hits.total.value === 0) { + return null; + } + + return response.hits.hits?.[0]._source.event?.dataset; +}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 17fc46b41278af..8ab3fdccbe72b8 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { union } from 'lodash'; +import { union, uniq } from 'lodash'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { @@ -16,6 +16,7 @@ import { import { createMetricModel } from './create_metrics_model'; import { JsonObject } from '../../../../common/typed_json'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; +import { getDatasetForField } from './get_dataset_for_field'; export const populateSeriesWithTSVBData = ( request: KibanaRequest, @@ -53,6 +54,12 @@ export const populateSeriesWithTSVBData = ( // Create the TSVB model based on the request options const model = createMetricModel(options); + const modules = await Promise.all( + uniq(options.metrics.filter(m => m.field)).map( + async m => + await getDatasetForField(framework, requestContext, m.field as string, options.indexPattern) + ) + ); const calculatedInterval = await calculateMetricInterval( framework, requestContext, @@ -61,14 +68,7 @@ export const populateSeriesWithTSVBData = ( timestampField: options.timerange.field, timerange: options.timerange, }, - options.metrics - .filter(metric => metric.field) - .map(metric => { - return metric - .field!.split(/\./) - .slice(0, 2) - .join('.'); - }) + modules.filter(m => m) as string[] ); if (calculatedInterval) { From 194c97f4f6ef728012ac85697aee5237472813e2 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 6 Jan 2020 14:12:07 -0500 Subject: [PATCH 004/137] Reorganizing spaces client-side plugin (#53644) * reorganizing spaces client-side plugin * additional testing and cleanup * address PR feedback * additional cleanup * rename scss file * one more index --- .../new_platform/new_platform.karma_mock.js | 2 + .../privilege_matrix.tsx | 2 +- .../privilege_space_table.tsx | 2 +- .../space_selector.tsx | 2 +- .../spaces_popover_list.tsx | 6 +- x-pack/legacy/plugins/spaces/index.ts | 4 +- .../advanced_settings_service.test.tsx | 40 +++++++ .../advanced_settings_service.tsx | 28 +++++ .../advanced_settings_subtitle.test.tsx | 0 .../advanced_settings_subtitle.tsx | 2 +- .../advanced_settings_subtitle/index.ts | 0 .../advanced_settings_title.test.tsx | 2 +- .../advanced_settings_title.tsx | 4 +- .../advanced_settings_title/index.ts | 0 .../advanced_settings/components/index.ts | 8 ++ .../spaces/public/advanced_settings/index.ts | 7 ++ .../spaces/public/{lib => }/constants.ts | 0 .../copy_saved_objects_to_space/_index.scss | 1 + .../components/_copy_to_space.scss} | 0 .../components/_index.scss | 1 + .../components}/copy_status_indicator.tsx | 5 +- .../copy_status_summary_indicator.tsx | 4 +- .../components}/copy_to_space_flyout.test.tsx | 11 +- .../components}/copy_to_space_flyout.tsx | 10 +- .../copy_to_space_flyout_footer.tsx | 4 +- .../components}/copy_to_space_form.tsx | 4 +- .../components}/index.ts | 0 .../components}/processing_copy_to_space.tsx | 10 +- .../components}/selectable_spaces_control.tsx | 4 +- .../components}/space_result.tsx | 10 +- .../components}/space_result_details.tsx | 8 +- .../copy_saved_objects_to_space_action.tsx | 4 +- ...opy_saved_objects_to_space_service.test.ts | 37 ++++++ .../copy_saved_objects_to_space_service.ts | 21 ++++ .../copy_saved_objects_to_space/index.ts | 2 +- .../summarize_copy_result.test.ts | 2 +- .../summarize_copy_result.ts | 4 +- .../copy_saved_objects_to_space/types.ts | 0 .../public/create_feature_catalogue_entry.ts | 2 +- .../legacy/plugins/spaces/public/index.scss | 5 +- x-pack/legacy/plugins/spaces/public/index.ts | 2 + x-pack/legacy/plugins/spaces/public/legacy.ts | 19 ++- .../spaces/public/management/_index.scss | 4 + .../confirm_delete_modal.test.tsx.snap | 0 .../_confirm_delete_modal.scss | 0 .../confirm_delete_modal.test.tsx | 4 +- .../confirm_delete_modal.tsx | 4 +- .../components/confirm_delete_modal/index.ts | 7 ++ .../management/components/index.ts | 1 + .../secure_space_message.test.tsx.snap | 0 .../components/secure_space_message/index.ts | 0 .../secure_space_message.test.tsx | 2 +- .../secure_space_message.tsx | 2 +- .../unauthorized_prompt.test.tsx.snap | 0 .../components/unauthorized_prompt/index.ts | 7 ++ .../unauthorized_prompt.test.tsx | 0 .../unauthorized_prompt.tsx | 0 .../delete_spaces_button.test.tsx.snap | 0 ...irm_alter_active_space_modal.test.tsx.snap | 0 .../confirm_alter_active_space_modal.test.tsx | 0 .../confirm_alter_active_space_modal.tsx | 0 .../confirm_alter_active_space_modal/index.ts | 0 .../customize_space_avatar.test.tsx.snap | 0 .../space_identifier.test.tsx.snap | 0 .../customize_space/customize_space.tsx | 6 +- .../customize_space_avatar.test.tsx | 0 .../customize_space_avatar.tsx | 10 +- .../edit_space/customize_space/index.ts | 0 .../customize_space/space_identifier.test.tsx | 0 .../customize_space/space_identifier.tsx | 2 +- .../edit_space/delete_spaces_button.test.tsx | 4 +- .../edit_space/delete_spaces_button.tsx | 5 +- .../enabled_features.test.tsx.snap | 0 .../edit_space/enabled_features/_index.scss | 0 .../enabled_features.test.tsx | 4 +- .../enabled_features/enabled_features.tsx | 4 +- .../enabled_features/feature_table.tsx | 8 +- .../edit_space/enabled_features/index.ts | 0 .../enabled_features/toggle_all_features.tsx | 0 .../management/edit_space/index.ts | 0 .../edit_space/manage_space_page.test.tsx | 20 +++- .../edit_space/manage_space_page.tsx | 22 ++-- .../edit_space/reserved_space_badge.test.tsx | 0 .../edit_space/reserved_space_badge.tsx | 4 +- .../__snapshots__/section_panel.test.tsx.snap | 0 .../section_panel/_section_panel.scss | 0 .../edit_space/section_panel/index.ts | 0 .../section_panel/section_panel.test.tsx | 0 .../section_panel/section_panel.tsx | 0 .../plugins/spaces/public/management/index.ts | 7 ++ .../legacy_page_routes.tsx} | 47 ++++++-- .../management/lib/feature_utils.test.ts | 2 +- .../management/lib/feature_utils.ts | 4 +- .../{views => }/management/lib/index.ts | 2 - .../lib/space_identifier_utils.test.ts | 0 .../management/lib/space_identifier_utils.ts | 0 .../management/lib/validate_space.test.ts | 0 .../management/lib/validate_space.ts | 4 +- .../management/management_service.test.ts | 113 ++++++++++++++++++ .../public/management/management_service.ts | 37 ++++++ .../spaces_grid_pages.test.tsx.snap | 0 .../management/spaces_grid/index.ts | 0 .../spaces_grid/spaces_grid_page.tsx | 14 +-- .../spaces_grid/spaces_grid_pages.test.tsx | 8 +- .../{views => }/management/template.html | 0 .../nav_control_popover.test.tsx.snap | 0 .../{views => }/nav_control/_index.scss | 1 + .../{views => }/nav_control/_nav_control.scss | 0 .../manage_spaces_button.test.tsx.snap | 0 .../spaces_description.test.tsx.snap | 0 .../nav_control/components/_index.scss | 0 .../components/_spaces_description.scss | 0 .../nav_control/components/_spaces_menu.scss | 0 .../components/manage_spaces_button.test.tsx | 0 .../components/manage_spaces_button.tsx | 2 +- .../components/spaces_description.test.tsx | 0 .../components/spaces_description.tsx | 4 +- .../nav_control/components/spaces_menu.tsx | 7 +- .../public/{views => }/nav_control/index.ts | 0 .../{views => }/nav_control/nav_control.tsx | 2 +- .../nav_control/nav_control_popover.test.tsx | 7 +- .../nav_control/nav_control_popover.tsx | 6 +- .../public/{views => }/nav_control/types.tsx | 0 .../legacy/plugins/spaces/public/plugin.tsx | 57 +++++++-- .../__snapshots__/space_avatar.test.tsx.snap | 0 .../{components => space_avatar}/index.ts | 2 +- .../space_attributes.test.ts | 0 .../{lib => space_avatar}/space_attributes.ts | 0 .../space_avatar.test.tsx | 0 .../space_avatar.tsx | 2 +- .../space_selector.test.tsx.snap | 0 .../spaces/public/space_selector/_index.scss | 2 + .../space_selector/_space_selector.scss | 0 .../components/_index.scss | 0 .../components/_space_card.scss | 0 .../components/_space_cards.scss | 0 .../components/index.ts | 0 .../components/space_card.test.tsx | 0 .../components/space_card.tsx | 8 +- .../components/space_cards.test.tsx | 0 .../components/space_cards.tsx | 0 .../{views => }/space_selector/index.tsx | 8 +- .../space_selector/space_selector.html | 0 .../space_selector/space_selector.test.tsx | 4 +- .../space_selector/space_selector.tsx | 8 +- .../public/{lib => spaces_manager}/index.ts | 1 - .../public/{lib => spaces_manager}/mocks.ts | 0 .../spaces_manager.mock.ts | 6 +- .../spaces_manager.test.ts | 0 .../{lib => spaces_manager}/spaces_manager.ts | 2 +- .../plugins/spaces/public/views/_index.scss | 4 - .../public/views/management/_index.scss | 3 - .../spaces/public/views/management/index.tsx | 77 ------------ .../views/management/lib/breadcrumbs.ts | 36 ------ .../public/views/space_selector/_index.scss | 1 - 155 files changed, 589 insertions(+), 298 deletions(-) create mode 100644 x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx create mode 100644 x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx rename x-pack/legacy/plugins/spaces/public/{views/management => advanced_settings}/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views/management => advanced_settings}/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{views/management => advanced_settings}/components/advanced_settings_subtitle/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views/management => advanced_settings}/components/advanced_settings_title/advanced_settings_title.test.tsx (94%) rename x-pack/legacy/plugins/spaces/public/{views/management => advanced_settings}/components/advanced_settings_title/advanced_settings_title.tsx (90%) rename x-pack/legacy/plugins/spaces/public/{views/management => advanced_settings}/components/advanced_settings_title/index.ts (100%) create mode 100644 x-pack/legacy/plugins/spaces/public/advanced_settings/components/index.ts create mode 100644 x-pack/legacy/plugins/spaces/public/advanced_settings/index.ts rename x-pack/legacy/plugins/spaces/public/{lib => }/constants.ts (100%) create mode 100644 x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/_index.scss rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space/_index.scss => copy_saved_objects_to_space/components/_copy_to_space.scss} (100%) create mode 100644 x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_index.scss rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/copy_status_indicator.tsx (96%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/copy_status_summary_indicator.tsx (94%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/copy_to_space_flyout.test.tsx (97%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/copy_to_space_flyout.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/copy_to_space_flyout_footer.tsx (97%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/copy_to_space_form.tsx (94%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/processing_copy_to_space.tsx (90%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/selectable_spaces_control.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/space_result.tsx (86%) rename x-pack/legacy/plugins/spaces/public/{views/management/components/copy_saved_objects_to_space => copy_saved_objects_to_space/components}/space_result_details.tsx (93%) rename x-pack/legacy/plugins/spaces/public/{lib => }/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx (89%) create mode 100644 x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts create mode 100644 x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts rename x-pack/legacy/plugins/spaces/public/{lib => }/copy_saved_objects_to_space/index.ts (74%) rename x-pack/legacy/plugins/spaces/public/{lib => }/copy_saved_objects_to_space/summarize_copy_result.test.ts (98%) rename x-pack/legacy/plugins/spaces/public/{lib => }/copy_saved_objects_to_space/summarize_copy_result.ts (96%) rename x-pack/legacy/plugins/spaces/public/{lib => }/copy_saved_objects_to_space/types.ts (100%) create mode 100644 x-pack/legacy/plugins/spaces/public/management/_index.scss rename x-pack/legacy/plugins/spaces/public/{views/management/components => management/components/confirm_delete_modal}/__snapshots__/confirm_delete_modal.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views/management/components => management/components/confirm_delete_modal}/_confirm_delete_modal.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views/management/components => management/components/confirm_delete_modal}/confirm_delete_modal.test.tsx (94%) rename x-pack/legacy/plugins/spaces/public/{views/management/components => management/components/confirm_delete_modal}/confirm_delete_modal.tsx (98%) create mode 100644 x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/index.ts rename x-pack/legacy/plugins/spaces/public/{views => }/management/components/index.ts (85%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/components/secure_space_message/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/components/secure_space_message/secure_space_message.test.tsx (93%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/components/secure_space_message/secure_space_message.tsx (94%) rename x-pack/legacy/plugins/spaces/public/{views/management/components => management/components/unauthorized_prompt}/__snapshots__/unauthorized_prompt.test.tsx.snap (100%) create mode 100644 x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/index.ts rename x-pack/legacy/plugins/spaces/public/{views/management/components => management/components/unauthorized_prompt}/unauthorized_prompt.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views/management/components => management/components/unauthorized_prompt}/unauthorized_prompt.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/confirm_alter_active_space_modal/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/customize_space.tsx (97%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/customize_space_avatar.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/customize_space_avatar.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/space_identifier.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/customize_space/space_identifier.tsx (98%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/delete_spaces_button.test.tsx (88%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/delete_spaces_button.tsx (96%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/enabled_features/_index.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/enabled_features/enabled_features.test.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/enabled_features/enabled_features.tsx (97%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/enabled_features/feature_table.tsx (92%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/enabled_features/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/enabled_features/toggle_all_features.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/manage_space_page.test.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/manage_space_page.tsx (93%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/reserved_space_badge.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/reserved_space_badge.tsx (89%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/section_panel/_section_panel.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/section_panel/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/section_panel/section_panel.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/edit_space/section_panel/section_panel.tsx (100%) create mode 100644 x-pack/legacy/plugins/spaces/public/management/index.ts rename x-pack/legacy/plugins/spaces/public/{views/management/page_routes.tsx => management/legacy_page_routes.tsx} (75%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/lib/feature_utils.test.ts (94%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/lib/feature_utils.ts (74%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/lib/index.ts (80%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/lib/space_identifier_utils.test.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/lib/space_identifier_utils.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/lib/validate_space.test.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/lib/validate_space.ts (96%) create mode 100644 x-pack/legacy/plugins/spaces/public/management/management_service.test.ts create mode 100644 x-pack/legacy/plugins/spaces/public/management/management_service.ts rename x-pack/legacy/plugins/spaces/public/{views => }/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/spaces_grid/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/spaces_grid/spaces_grid_page.tsx (96%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/spaces_grid/spaces_grid_pages.test.tsx (90%) rename x-pack/legacy/plugins/spaces/public/{views => }/management/template.html (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/__snapshots__/nav_control_popover.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/_index.scss (55%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/_nav_control.scss (100%) rename x-pack/legacy/plugins/spaces/public/{ => nav_control}/components/__snapshots__/manage_spaces_button.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/components/__snapshots__/spaces_description.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/components/_index.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/components/_spaces_description.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/components/_spaces_menu.scss (100%) rename x-pack/legacy/plugins/spaces/public/{ => nav_control}/components/manage_spaces_button.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{ => nav_control}/components/manage_spaces_button.tsx (96%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/components/spaces_description.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/components/spaces_description.tsx (89%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/components/spaces_menu.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/nav_control.tsx (94%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/nav_control_popover.test.tsx (91%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/nav_control_popover.tsx (96%) rename x-pack/legacy/plugins/spaces/public/{views => }/nav_control/types.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{components => space_avatar}/__snapshots__/space_avatar.test.tsx.snap (100%) rename x-pack/legacy/plugins/spaces/public/{components => space_avatar}/index.ts (82%) rename x-pack/legacy/plugins/spaces/public/{lib => space_avatar}/space_attributes.test.ts (100%) rename x-pack/legacy/plugins/spaces/public/{lib => space_avatar}/space_attributes.ts (100%) rename x-pack/legacy/plugins/spaces/public/{components => space_avatar}/space_avatar.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{components => space_avatar}/space_avatar.tsx (98%) rename x-pack/legacy/plugins/spaces/public/{views => }/space_selector/__snapshots__/space_selector.test.tsx.snap (100%) create mode 100644 x-pack/legacy/plugins/spaces/public/space_selector/_index.scss rename x-pack/legacy/plugins/spaces/public/{views => }/space_selector/_space_selector.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/_index.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/_space_card.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/_space_cards.scss (100%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/index.ts (100%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/space_card.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/space_card.tsx (90%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/space_cards.test.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => space_selector}/components/space_cards.tsx (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/space_selector/index.tsx (82%) rename x-pack/legacy/plugins/spaces/public/{views => }/space_selector/space_selector.html (100%) rename x-pack/legacy/plugins/spaces/public/{views => }/space_selector/space_selector.test.tsx (92%) rename x-pack/legacy/plugins/spaces/public/{views => }/space_selector/space_selector.tsx (95%) rename x-pack/legacy/plugins/spaces/public/{lib => spaces_manager}/index.ts (76%) rename x-pack/legacy/plugins/spaces/public/{lib => spaces_manager}/mocks.ts (100%) rename x-pack/legacy/plugins/spaces/public/{lib => spaces_manager}/spaces_manager.mock.ts (87%) rename x-pack/legacy/plugins/spaces/public/{lib => spaces_manager}/spaces_manager.test.ts (100%) rename x-pack/legacy/plugins/spaces/public/{lib => spaces_manager}/spaces_manager.ts (97%) delete mode 100644 x-pack/legacy/plugins/spaces/public/views/_index.scss delete mode 100644 x-pack/legacy/plugins/spaces/public/views/management/_index.scss delete mode 100644 x-pack/legacy/plugins/spaces/public/views/management/index.tsx delete mode 100644 x-pack/legacy/plugins/spaces/public/views/management/lib/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/spaces/public/views/space_selector/_index.scss diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 3d4292cef27f4c..06424ea48a40f9 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -148,6 +148,8 @@ export const npStart = { legacy: { getSection: () => ({ register: sinon.fake(), + deregister: sinon.fake(), + hasItem: sinon.fake(), }), }, }, diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx index 92dace65d466c4..962487312c83df 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx @@ -23,7 +23,7 @@ import { import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { SpaceAvatar } from '../../../../../../../../../spaces/public/components'; +import { SpaceAvatar } from '../../../../../../../../../spaces/public/space_avatar'; import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { FeaturesPrivileges, Role } from '../../../../../../../../common/model'; import { CalculatedPrivilege } from '../../../../../../../lib/kibana_privilege_calculator'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index e54b5ff9c45da7..65a3df9fb47a1a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -14,7 +14,7 @@ import { import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component } from 'react'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/lib/space_attributes'; +import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; import { Space } from '../../../../../../../../../spaces/common/model/space'; import { FeaturesPrivileges, diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx index e6e206e5fc7f46..0eb9cf0b0ee9d7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -8,7 +8,7 @@ import { EuiComboBox, EuiComboBoxOptionProps, EuiHealth, EuiHighlight } from '@e import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/lib/space_attributes'; +import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; const spaceToOption = (space?: Space, currentSelection?: 'global' | 'spaces') => { if (!space) { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx index 1ab2a27220eeea..a99e389044eaad 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx @@ -14,9 +14,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../../spaces/common/constants'; -import { Space } from '../../../../../../../spaces/common/model/space'; -import { SpaceAvatar } from '../../../../../../../spaces/public/components'; +import { SpaceAvatar } from '../../../../../../../spaces/public/space_avatar'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../../../../plugins/spaces/common/constants'; +import { Space } from '../../../../../../../../../plugins/spaces/common/model/space'; interface Props { spaces: Space[]; diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 0083847cfb441b..934b44b4accaff 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -49,12 +49,12 @@ export const spaces = (kibana: Record) => uiExports: { styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/spaces/views/management'], + managementSections: [], apps: [ { id: 'space_selector', title: 'Spaces', - main: 'plugins/spaces/views/space_selector', + main: 'plugins/spaces/space_selector', url: 'space_selector', hidden: true, }, diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx new file mode 100644 index 00000000000000..aa3c6acf262361 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx @@ -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 { AdvancedSettingsService } from './advanced_settings_service'; +jest.mock('ui/management', () => { + return { + PAGE_TITLE_COMPONENT: 'page_title_component', + PAGE_SUBTITLE_COMPONENT: 'page_subtitle_component', + }; +}); + +describe('Advanced Settings Service', () => { + describe('#setup', () => { + it('registers space-aware components to augment the advanced settings screen', () => { + const deps = { + getActiveSpace: jest.fn().mockResolvedValue({ id: 'foo', name: 'foo-space' }), + registerSettingsComponent: jest.fn(), + }; + + const advancedSettingsService = new AdvancedSettingsService(); + advancedSettingsService.setup(deps); + + expect(deps.registerSettingsComponent).toHaveBeenCalledTimes(2); + expect(deps.registerSettingsComponent).toHaveBeenCalledWith( + 'page_title_component', + expect.any(Function), + true + ); + + expect(deps.registerSettingsComponent).toHaveBeenCalledWith( + 'page_subtitle_component', + expect.any(Function), + true + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx new file mode 100644 index 00000000000000..9c6c2fcc2cddac --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx @@ -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 React from 'react'; +import { PAGE_TITLE_COMPONENT, PAGE_SUBTITLE_COMPONENT } from 'ui/management'; +import { Space } from '../../common/model/space'; +import { AdvancedSettingsTitle, AdvancedSettingsSubtitle } from './components'; + +interface SetupDeps { + getActiveSpace: () => Promise; + registerSettingsComponent: ( + id: string, + component: string | React.FC, + allowOverride: boolean + ) => void; +} + +export class AdvancedSettingsService { + public setup({ getActiveSpace, registerSettingsComponent }: SetupDeps) { + const PageTitle = () => ; + const SubTitle = () => ; + + registerSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle, true); + registerSettingsComponent(PAGE_SUBTITLE_COMPONENT, SubTitle, true); + } +} diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx index 433f8a8ccf0a20..e35d67c7214cf1 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx @@ -7,7 +7,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment, useState, useEffect } from 'react'; -import { Space } from '../../../../../common/model/space'; +import { Space } from '../../../../common/model/space'; interface Props { getActiveSpace: () => Promise; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/index.ts rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx index bf792ca2cdacf7..b772ff433abecd 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { AdvancedSettingsTitle } from './advanced_settings_title'; -import { SpaceAvatar } from '../../../../components'; +import { SpaceAvatar } from '../../../space_avatar'; import { act } from '@testing-library/react'; describe('AdvancedSettingsTitle', () => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx index af6fa42cce07be..b74524db81d812 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx @@ -7,8 +7,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState, useEffect } from 'react'; -import { Space } from '../../../../../common/model/space'; -import { SpaceAvatar } from '../../../../components'; +import { Space } from '../../../../../../../plugins/spaces/common/model/space'; +import { SpaceAvatar } from '../../../space_avatar'; interface Props { getActiveSpace: () => Promise; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/index.ts rename to x-pack/legacy/plugins/spaces/public/advanced_settings/components/advanced_settings_title/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/components/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/index.ts new file mode 100644 index 00000000000000..6678be7fa34e46 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/components/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { AdvancedSettingsSubtitle } from './advanced_settings_subtitle'; +export { AdvancedSettingsTitle } from './advanced_settings_title'; diff --git a/x-pack/legacy/plugins/spaces/public/advanced_settings/index.ts b/x-pack/legacy/plugins/spaces/public/advanced_settings/index.ts new file mode 100644 index 00000000000000..546831a84fa82a --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/advanced_settings/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 { AdvancedSettingsService } from './advanced_settings_service'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/constants.ts b/x-pack/legacy/plugins/spaces/public/constants.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/constants.ts rename to x-pack/legacy/plugins/spaces/public/constants.ts diff --git a/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/_index.scss b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/_index.scss new file mode 100644 index 00000000000000..30ac0c9fe9b27d --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/_index.scss @@ -0,0 +1 @@ +@import './components/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/_index.scss b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_copy_to_space.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/_index.scss rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_copy_to_space.scss diff --git a/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_index.scss b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_index.scss new file mode 100644 index 00000000000000..92b19a8c8c6a3e --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/_index.scss @@ -0,0 +1 @@ +@import './copy_to_space'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_indicator.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_indicator.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx index f9da25409d60e4..ff9035ff02be5b 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_indicator.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx @@ -7,10 +7,7 @@ import React from 'react'; import { EuiLoadingSpinner, EuiText, EuiIconTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - SummarizedCopyToSpaceResult, - SummarizedSavedObjectResult, -} from '../../../../lib/copy_saved_objects_to_space'; +import { SummarizedCopyToSpaceResult, SummarizedSavedObjectResult } from '..'; interface Props { summarizedCopyResult: SummarizedCopyToSpaceResult; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx index 0ad5f72ba3e45f..9d73c216c73cec 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { EuiLoadingSpinner, EuiIconTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Space } from '../../../../../common/model/space'; -import { SummarizedCopyToSpaceResult } from '../../../../lib/copy_saved_objects_to_space'; +import { Space } from '../../../common/model/space'; +import { SummarizedCopyToSpaceResult } from '..'; interface Props { space: Space; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index b3fd345b1d2b4f..28011911e212eb 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -6,20 +6,20 @@ import React from 'react'; import Boom from 'boom'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../src/legacy/core_plugins/management/public/np_ready/mocks'; +import { mockManagementPlugin } from '../../../../../../../src/legacy/core_plugins/management/public/np_ready/mocks'; import { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout'; import { CopyToSpaceForm } from './copy_to_space_form'; import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; -import { Space } from '../../../../../common/model/space'; +import { Space } from '../../../common/model/space'; import { findTestSubject } from 'test_utils/find_test_subject'; import { SelectableSpacesControl } from './selectable_spaces_control'; import { act } from '@testing-library/react'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; -import { spacesManagerMock } from '../../../../lib/mocks'; -import { SpacesManager } from '../../../../lib'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; -jest.mock('../../../../../../../../../src/legacy/core_plugins/management/public/legacy', () => ({ +jest.mock('../../../../../../../src/legacy/core_plugins/management/public/legacy', () => ({ setup: mockManagementPlugin.createSetupContract(), start: mockManagementPlugin.createStartContract(), })); @@ -404,6 +404,7 @@ describe('CopyToSpaceFlyout', () => { id: 'my-viz', error: { type: 'missing_references', + blocking: [], references: [{ type: 'index-pattern', id: 'missing-index-pattern' }], }, }, diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 5a43e5878ab83f..f486f2f24f13df 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -22,17 +22,17 @@ import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; import { ProcessedImportResponse, processImportResponse, -} from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { Space } from '../../../../../common/model/space'; -import { SpacesManager } from '../../../../lib'; +} from '../../../../../../../src/legacy/core_plugins/management/public'; +import { Space } from '../../../common/model/space'; +import { SpacesManager } from '../../spaces_manager'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; -import { CopyOptions, ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { CopyOptions, ImportRetry } from '../types'; interface Props { onClose: () => void; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx index 5853bebe3c6697..56f39ce3ed4fb0 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx @@ -8,8 +8,8 @@ import React, { Fragment } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiStat, EuiHorizontalRule } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ProcessedImportResponse } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { ImportRetry } from '../types'; interface Props { copyInProgress: boolean; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index 2a7e17c253f0b6..f680793e27fe06 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -14,8 +14,8 @@ import { EuiListGroupItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CopyOptions } from '../../../../lib/copy_saved_objects_to_space/types'; -import { Space } from '../../../../../common/model/space'; +import { CopyOptions } from '../types'; +import { Space } from '../../../common/model/space'; import { SelectableSpacesControl } from './selectable_spaces_control'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index b04c9598559b3e..285abb828a0116 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -13,12 +13,12 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { ProcessedImportResponse } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { summarizeCopyResult } from '../../../../lib/copy_saved_objects_to_space'; -import { Space } from '../../../../../common/model/space'; -import { CopyOptions, ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { Space } from '../../../common/model/space'; +import { CopyOptions, ImportRetry } from '../types'; import { SpaceResult } from './space_result'; +import { summarizeCopyResult } from '..'; interface Props { savedObject: SavedObjectsManagementRecord; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx index 42d57075313800..9cf81b1cc4486d 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -6,8 +6,8 @@ import React, { Fragment, useState } from 'react'; import { EuiSelectable, EuiLoadingSpinner } from '@elastic/eui'; -import { SpaceAvatar } from '../../../../components'; -import { Space } from '../../../../../common/model/space'; +import { SpaceAvatar } from '../../space_avatar'; +import { Space } from '../../../common/model/space'; interface Props { spaces: Space[]; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx similarity index 86% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx index f71be12276be51..22f0767ba196e3 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { SummarizedCopyToSpaceResult } from '../../../../lib/copy_saved_objects_to_space'; -import { SpaceAvatar } from '../../../../components'; -import { Space } from '../../../../../common/model/space'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { SummarizedCopyToSpaceResult } from '../index'; +import { SpaceAvatar } from '../../space_avatar'; +import { Space } from '../../../common/model/space'; import { CopyStatusSummaryIndicator } from './copy_status_summary_indicator'; import { SpaceCopyResultDetails } from './space_result_details'; -import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { ImportRetry } from '../types'; interface Props { savedObject: SavedObjectsManagementRecord; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx similarity index 93% rename from x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx index 66ec38331c89ae..d3ab406b87c3e3 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx @@ -5,13 +5,13 @@ */ import React from 'react'; -import { SummarizedCopyToSpaceResult } from 'plugins/spaces/lib/copy_saved_objects_to_space'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SavedObjectsManagementRecord } from '../../../../../../../../../src/legacy/core_plugins/management/public'; -import { Space } from '../../../../../common/model/space'; +import { SummarizedCopyToSpaceResult } from '../index'; +import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { Space } from '../../../common/model/space'; import { CopyStatusIndicator } from './copy_status_indicator'; -import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types'; +import { ImportRetry } from '../types'; interface Props { savedObject: SavedObjectsManagementRecord; diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx similarity index 89% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx index 3b0fffa38e7856..c016494a4cdf9e 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx @@ -9,8 +9,8 @@ import { toastNotifications } from 'ui/notify'; import { SavedObjectsManagementAction, SavedObjectsManagementRecord, -} from '../../../../../../../src/legacy/core_plugins/management/public'; -import { CopySavedObjectsToSpaceFlyout } from '../../views/management/components/copy_saved_objects_to_space'; +} from '../../../../../../src/legacy/core_plugins/management/public'; +import { CopySavedObjectsToSpaceFlyout } from './components'; import { SpacesManager } from '../spaces_manager'; export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction { diff --git a/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts new file mode 100644 index 00000000000000..63a59344dfe5d2 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts @@ -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 { ManagementSetup } from 'src/legacy/core_plugins/management/public'; +import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; +import { spacesManagerMock } from '../spaces_manager/mocks'; +import { CopySavedObjectsToSpaceService } from '.'; + +describe('CopySavedObjectsToSpaceService', () => { + describe('#setup', () => { + it('registers the CopyToSpaceSavedObjectsManagementAction', () => { + const deps = { + spacesManager: spacesManagerMock.create(), + // we don't have a proper NP mock for this yet + managementSetup: ({ + savedObjects: { + registry: { + has: jest.fn().mockReturnValue(false), + register: jest.fn(), + }, + }, + } as unknown) as ManagementSetup, + }; + + const service = new CopySavedObjectsToSpaceService(); + service.setup(deps); + + expect(deps.managementSetup.savedObjects.registry.register).toHaveBeenCalledTimes(1); + expect(deps.managementSetup.savedObjects.registry.register).toHaveBeenCalledWith( + expect.any(CopyToSpaceSavedObjectsManagementAction) + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts new file mode 100644 index 00000000000000..37354f985a2fc8 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.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 { ManagementSetup } from 'src/legacy/core_plugins/management/public'; +import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; +import { SpacesManager } from '../spaces_manager'; + +interface SetupDeps { + spacesManager: SpacesManager; + managementSetup: ManagementSetup; +} + +export class CopySavedObjectsToSpaceService { + public setup({ spacesManager, managementSetup }: SetupDeps) { + const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager); + managementSetup.savedObjects.registry.register(action); + } +} diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/index.ts similarity index 74% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/index.ts index be23d90cc242a6..06969a52a3d8d4 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/index.ts @@ -5,4 +5,4 @@ */ export * from './summarize_copy_result'; -export { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; +export { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space_service'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts similarity index 98% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts index 0352902072790e..0244a35711e6fb 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.test.ts @@ -5,7 +5,7 @@ */ import { summarizeCopyResult } from './summarize_copy_result'; -import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; const createSavedObjectsManagementRecord = () => ({ type: 'dashboard', diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts similarity index 96% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts index 8807489157d71e..7bc47d35efc6c2 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts +++ b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/summarize_copy_result.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsManagementRecord } from '../../../../../../../src/legacy/core_plugins/management/public'; -import { ProcessedImportResponse } from '../../../../../../../src/legacy/core_plugins/management/public'; +import { SavedObjectsManagementRecord } from '../../../../../../src/legacy/core_plugins/management/public'; +import { ProcessedImportResponse } from '../../../../../../src/legacy/core_plugins/management/public'; export interface SummarizedSavedObjectResult { type: string; diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts b/x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/types.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts rename to x-pack/legacy/plugins/spaces/public/copy_saved_objects_to_space/types.ts diff --git a/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts b/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts index 1f41bb89d77076..464066d2221de1 100644 --- a/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts +++ b/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts @@ -9,7 +9,7 @@ import { FeatureCatalogueEntry, FeatureCatalogueCategory, } from '../../../../../src/plugins/home/public'; -import { getSpacesFeatureDescription } from './lib/constants'; +import { getSpacesFeatureDescription } from './constants'; export const createSpacesFeatureCatalogueEntry = (): FeatureCatalogueEntry => { return { diff --git a/x-pack/legacy/plugins/spaces/public/index.scss b/x-pack/legacy/plugins/spaces/public/index.scss index 7a40872b760cb7..26269f1d31aa3d 100644 --- a/x-pack/legacy/plugins/spaces/public/index.scss +++ b/x-pack/legacy/plugins/spaces/public/index.scss @@ -10,4 +10,7 @@ // spcChart__legend--small // spcChart__legend-isLoading -@import './views/index'; +@import './management/index'; +@import './nav_control/index'; +@import './space_selector/index'; +@import './copy_saved_objects_to_space/index'; diff --git a/x-pack/legacy/plugins/spaces/public/index.ts b/x-pack/legacy/plugins/spaces/public/index.ts index 9233aae9fb12fb..53cb906a619d32 100644 --- a/x-pack/legacy/plugins/spaces/public/index.ts +++ b/x-pack/legacy/plugins/spaces/public/index.ts @@ -5,6 +5,8 @@ */ import { SpacesPlugin } from './plugin'; +export { SpaceAvatar } from './space_avatar'; + export const plugin = () => { return new SpacesPlugin(); }; diff --git a/x-pack/legacy/plugins/spaces/public/legacy.ts b/x-pack/legacy/plugins/spaces/public/legacy.ts index 99419206093e93..1dffbd26617144 100644 --- a/x-pack/legacy/plugins/spaces/public/legacy.ts +++ b/x-pack/legacy/plugins/spaces/public/legacy.ts @@ -4,15 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { registerSettingsComponent } from 'ui/management'; import { npSetup, npStart } from 'ui/new_platform'; +import { setup as managementSetup } from '../../../../../src/legacy/core_plugins/management/public/legacy'; import { plugin } from '.'; -import { SpacesPlugin, PluginsSetup } from './plugin'; +import { SpacesPlugin, PluginsSetup, PluginsStart } from './plugin'; +import './management/legacy_page_routes'; const spacesPlugin: SpacesPlugin = plugin(); -const plugins: PluginsSetup = { +const pluginsSetup: PluginsSetup = { home: npSetup.plugins.home, + management: managementSetup, + __managementLegacyCompat: { + registerSettingsComponent, + }, }; -export const setup = spacesPlugin.setup(npSetup.core, plugins); -export const start = spacesPlugin.start(npStart.core); +const pluginsStart: PluginsStart = { + management: npStart.plugins.management, +}; + +export const setup = spacesPlugin.setup(npSetup.core, pluginsSetup); +export const start = spacesPlugin.start(npStart.core, pluginsStart); diff --git a/x-pack/legacy/plugins/spaces/public/management/_index.scss b/x-pack/legacy/plugins/spaces/public/management/_index.scss new file mode 100644 index 00000000000000..72deb1f1cde8d0 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/_index.scss @@ -0,0 +1,4 @@ +@import './components/confirm_delete_modal/confirm_delete_modal'; +@import './edit_space/enabled_features/index'; +@import './edit_space/section_panel/section_panel'; + diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/_confirm_delete_modal.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/_confirm_delete_modal.scss rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/_confirm_delete_modal.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx index f0ab2c99ac2e20..331435b54edb70 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ConfirmDeleteModal } from './confirm_delete_modal'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { spacesManagerMock } from '../../../spaces_manager/mocks'; +import { SpacesManager } from '../../../spaces_manager'; describe('ConfirmDeleteModal', () => { it('renders as expected', () => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx similarity index 98% rename from x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index 0c76cb4a828fe2..6eed58a7842124 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -25,8 +25,8 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; -import { Space } from '../../../../common/model/space'; -import { SpacesManager } from '../../../lib'; +import { SpacesManager } from '../../../spaces_manager'; +import { Space } from '../../../../../../plugins/spaces/common/model/space'; interface Props { space: Space; diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/index.ts new file mode 100644 index 00000000000000..651455d00e9f28 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/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 { ConfirmDeleteModal } from './confirm_delete_modal'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/index.ts similarity index 85% rename from x-pack/legacy/plugins/spaces/public/views/management/components/index.ts rename to x-pack/legacy/plugins/spaces/public/management/components/index.ts index 91f4964e1da068..7f9f80f470d12a 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/index.ts +++ b/x-pack/legacy/plugins/spaces/public/management/components/index.ts @@ -6,3 +6,4 @@ export { ConfirmDeleteModal } from './confirm_delete_modal'; export { UnauthorizedPrompt } from './unauthorized_prompt'; +export { SecureSpaceMessage } from './secure_space_message'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/__snapshots__/secure_space_message.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/index.ts rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.test.tsx similarity index 93% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.test.tsx index 1cc6f6c1f72bee..b43010fe5f326a 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.test.tsx @@ -8,7 +8,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { SecureSpaceMessage } from './secure_space_message'; let mockShowLinks: boolean = true; -jest.mock('../../../../../../xpack_main/public/services/xpack_info', () => { +jest.mock('../../../../../xpack_main/public/services/xpack_info', () => { return { xpackInfo: { get: jest.fn().mockImplementation((key: string) => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx index 6bbc423968b4bf..746b7e2ac4c986 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; // @ts-ignore -import { xpackInfo } from '../../../../../../xpack_main/public/services/xpack_info'; +import { xpackInfo } from '../../../../../xpack_main/public/services/xpack_info'; export const SecureSpaceMessage = ({}) => { const showSecurityLinks = xpackInfo.get('features.security.showLinks'); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/unauthorized_prompt.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/index.ts b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/index.ts new file mode 100644 index 00000000000000..5a8120a77804bc --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/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 { UnauthorizedPrompt } from './unauthorized_prompt'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx b/x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/components/unauthorized_prompt.tsx rename to x-pack/legacy/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/__snapshots__/delete_spaces_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx index 8a7e384d44b358..b0d74afaa90aa5 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx @@ -18,9 +18,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { isReservedSpace } from '../../../../../common'; -import { Space } from '../../../../../common/model/space'; -import { SpaceAvatar } from '../../../../components'; +import { isReservedSpace } from '../../../../common'; +import { Space } from '../../../../common/model/space'; +import { SpaceAvatar } from '../../../space_avatar'; import { SpaceValidator, toSpaceIdentifier } from '../../lib'; import { SectionPanel } from '../section_panel'; import { CustomizeSpaceAvatar } from './customize_space_avatar'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx index 12fa0193b59a4e..c3207c82bf95e1 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx @@ -17,12 +17,10 @@ import { isValidHex, } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; - -import { getSpaceColor, getSpaceInitials } from '../../../../lib/space_attributes'; -import { encode, imageTypes } from '../../../../../common/lib/dataurl'; - -import { MAX_SPACE_INITIALS } from '../../../../../common/constants'; -import { Space } from '../../../../../common/model/space'; +import { imageTypes, encode } from '../../../../common/lib/dataurl'; +import { getSpaceColor, getSpaceInitials } from '../../../space_avatar'; +import { Space } from '../../../../../../../plugins/spaces/common/model/space'; +import { MAX_SPACE_INITIALS } from '../../../../../../../plugins/spaces/common'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.tsx similarity index 98% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.tsx index a717570b19c5df..1d6664273d21ea 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/space_identifier.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/customize_space/space_identifier.tsx @@ -7,7 +7,7 @@ import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { Space } from '../../../../../common/model/space'; +import { Space } from '../../../../common/model/space'; import { SpaceValidator, toSpaceIdentifier } from '../../lib'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx similarity index 88% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx index e7c7dfc5eb1b05..364145d6495b84 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DeleteSpacesButton } from './delete_spaces_button'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; const space = { id: 'my-space', diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx index 216dd7c41f124b..56a858eb4ccf67 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx @@ -7,10 +7,9 @@ import { EuiButton, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -// @ts-ignore import { toastNotifications } from 'ui/notify'; -import { Space } from '../../../../common/model/space'; -import { SpacesManager } from '../../../lib/spaces_manager'; +import { Space } from '../../../common/model/space'; +import { SpacesManager } from '../../spaces_manager'; import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/_index.scss b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/_index.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/_index.scss rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/_index.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx index f8bd4b889394ad..f770857d9313d6 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx @@ -7,8 +7,8 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { Space } from '../../../../../common/model/space'; +import { Feature } from '../../../../../../../plugins/features/public'; +import { Space } from '../../../../common/model/space'; import { SectionPanel } from '../section_panel'; import { EnabledFeatures } from './enabled_features'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx similarity index 97% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 628be759b7c5ce..70312296f757be 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -8,8 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment, ReactNode } from 'react'; import { Capabilities } from 'src/core/public'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { Space } from '../../../../../common/model/space'; +import { Feature } from '../../../../../../../plugins/features/public'; +import { Space } from '../../../../common/model/space'; import { getEnabledFeatures } from '../../lib/feature_utils'; import { SectionPanel } from '../section_panel'; import { FeatureTable } from './feature_table'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx similarity index 92% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index b3654b4d35bd3e..2866d0bfa8cf3c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import { EuiCheckbox, EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } from '@elastic/eui'; + +import { EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component } from 'react'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { Space } from '../../../../../common/model/space'; +import { Feature } from '../../../../../../../plugins/features/public'; +import { Space } from '../../../../common/model/space'; import { ToggleAllFeatures } from './toggle_all_features'; interface Props { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/toggle_all_features.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/toggle_all_features.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index c69a885ae05878..d24e932bce1126 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -6,7 +6,7 @@ jest.mock('ui/kfetch', () => ({ kfetch: () => Promise.resolve([{ id: 'feature-1', name: 'feature 1', icon: 'spacesApp' }]), })); -import '../../../__mocks__/xpack_info'; +import '../../__mocks__/xpack_info'; import { EuiButton, EuiLink, EuiSwitch } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; @@ -14,8 +14,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; import { ManageSpacePage } from './manage_space_page'; import { SectionPanel } from './section_panel'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; const space = { id: 'my-space', @@ -65,21 +65,28 @@ describe('ManageSpacePage', () => { }); it('allows a space to be updated', async () => { - const spacesManager = spacesManagerMock.create(); - spacesManager.getSpace = jest.fn().mockResolvedValue({ + const spaceToUpdate = { id: 'existing-space', name: 'Existing Space', description: 'hey an existing space', color: '#aabbcc', initials: 'AB', disabledFeatures: [], + }; + + const spacesManager = spacesManagerMock.create(); + spacesManager.getSpace = jest.fn().mockResolvedValue({ + ...spaceToUpdate, }); spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); + const onLoadSpace = jest.fn(); + const wrapper = mountWithIntl( { await waitForDataLoad(wrapper); expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space'); + expect(onLoadSpace).toHaveBeenCalledWith({ + ...spaceToUpdate, + }); await Promise.resolve(); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.tsx similarity index 93% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.tsx index a5d60d1a731ba2..6bbb32ccd654f0 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -17,17 +17,15 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { Breadcrumb } from 'ui/chrome'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; import { Capabilities } from 'src/core/public'; -import { Feature } from '../../../../../../../plugins/features/public'; -import { isReservedSpace } from '../../../../common'; -import { Space } from '../../../../common/model/space'; -import { SpacesManager } from '../../../lib'; -import { SecureSpaceMessage } from '../components/secure_space_message'; -import { UnauthorizedPrompt } from '../components/unauthorized_prompt'; -import { getEditBreadcrumbs, toSpaceIdentifier } from '../lib'; +import { Feature } from '../../../../../../plugins/features/public'; +import { isReservedSpace } from '../../../common'; +import { Space } from '../../../common/model/space'; +import { SpacesManager } from '../../spaces_manager'; +import { SecureSpaceMessage, UnauthorizedPrompt } from '../components'; +import { toSpaceIdentifier } from '../lib'; import { SpaceValidator } from '../lib/validate_space'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; import { CustomizeSpace } from './customize_space'; @@ -39,7 +37,7 @@ interface Props { spacesManager: SpacesManager; spaceId?: string; intl: InjectedIntl; - setBreadcrumbs?: (breadcrumbs: Breadcrumb[]) => void; + onLoadSpace?: (space: Space) => void; capabilities: Capabilities; } @@ -76,7 +74,7 @@ class ManageSpacePageUI extends Component { return; } - const { spaceId, spacesManager, intl, setBreadcrumbs } = this.props; + const { spaceId, spacesManager, intl, onLoadSpace } = this.props; const getFeatures = kfetch({ method: 'get', pathname: '/api/features' }); @@ -84,8 +82,8 @@ class ManageSpacePageUI extends Component { try { const [space, features] = await Promise.all([spacesManager.getSpace(spaceId), getFeatures]); if (space) { - if (setBreadcrumbs) { - setBreadcrumbs(getEditBreadcrumbs(space)); + if (onLoadSpace) { + onLoadSpace(space); } this.setState({ diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx similarity index 89% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx index e4b2dda3a668b5..38bf3519020967 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/reserved_space_badge.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { isReservedSpace } from '../../../../common'; -import { Space } from '../../../../common/model/space'; +import { isReservedSpace } from '../../../common'; +import { Space } from '../../../common/model/space'; interface Props { space?: Space; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/_section_panel.scss b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/_section_panel.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/_section_panel.scss rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/_section_panel.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/index.ts b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/index.ts rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.test.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.tsx b/x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.tsx rename to x-pack/legacy/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx diff --git a/x-pack/legacy/plugins/spaces/public/management/index.ts b/x-pack/legacy/plugins/spaces/public/management/index.ts new file mode 100644 index 00000000000000..ad3cc6b2456192 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/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 { ManagementService } from './management_service'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx b/x-pack/legacy/plugins/spaces/public/management/legacy_page_routes.tsx similarity index 75% rename from x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx rename to x-pack/legacy/plugins/spaces/public/management/legacy_page_routes.tsx index d8fd0298df2fc3..8cf4a129e5b8fb 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/legacy_page_routes.tsx @@ -4,30 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ // @ts-ignore -import template from 'plugins/spaces/views/management/template.html'; +import template from 'plugins/spaces/management/template.html'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nContext } from 'ui/i18n'; // @ts-ignore import routes from 'ui/routes'; +import { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; import { npStart } from 'ui/new_platform'; import { ManageSpacePage } from './edit_space'; -import { getCreateBreadcrumbs, getEditBreadcrumbs, getListBreadcrumbs } from './lib'; import { SpacesGridPage } from './spaces_grid'; -import { start as spacesNPStart } from '../../legacy'; +import { start as spacesNPStart } from '../legacy'; +import { Space } from '../../common/model/space'; const reactRootNodeId = 'manageSpacesReactRoot'; +function getListBreadcrumbs() { + return [ + MANAGEMENT_BREADCRUMB, + { + text: 'Spaces', + href: '#/management/spaces/list', + }, + ]; +} + +function getCreateBreadcrumbs() { + return [ + ...getListBreadcrumbs(), + { + text: 'Create', + }, + ]; +} + +function getEditBreadcrumbs(space?: Space) { + return [ + ...getListBreadcrumbs(), + { + text: space ? space.name : '...', + }, + ]; +} + routes.when('/management/spaces/list', { template, k7Breadcrumbs: getListBreadcrumbs, requireUICapability: 'management.kibana.spaces', controller($scope: any) { - $scope.$$postDigest(async () => { + $scope.$$postDigest(() => { const domNode = document.getElementById(reactRootNodeId); - const { spacesManager } = await spacesNPStart; + const { spacesManager } = spacesNPStart; render( @@ -54,10 +83,10 @@ routes.when('/management/spaces/create', { k7Breadcrumbs: getCreateBreadcrumbs, requireUICapability: 'management.kibana.spaces', controller($scope: any) { - $scope.$$postDigest(async () => { + $scope.$$postDigest(() => { const domNode = document.getElementById(reactRootNodeId); - const { spacesManager } = await spacesNPStart; + const { spacesManager } = spacesNPStart; render( @@ -100,7 +129,9 @@ routes.when('/management/spaces/edit/:spaceId', { { + npStart.core.chrome.setBreadcrumbs(getEditBreadcrumbs(space)); + }} capabilities={npStart.core.application.capabilities} /> , diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.test.ts similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.test.ts index 8621ec5614368c..ce874956d0ef29 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../../plugins/features/public'; import { getEnabledFeatures } from './feature_utils'; +import { Feature } from '../../../../../../plugins/features/public'; const buildFeatures = () => [ diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.ts similarity index 74% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.ts index ef46a539677442..ff1688637ef73c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/feature_utils.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../../plugins/features/public'; +import { Feature } from '../../../../../../plugins/features/common'; -import { Space } from '../../../../common/model/space'; +import { Space } from '../../../../../../plugins/spaces/common/model/space'; export function getEnabledFeatures(features: Feature[], space: Partial) { return features.filter(feature => !(space.disabledFeatures || []).includes(feature.id)); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/index.ts b/x-pack/legacy/plugins/spaces/public/management/lib/index.ts similarity index 80% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/index.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/index.ts index f65757f5dba262..4a158168febd8c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/index.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/index.ts @@ -7,5 +7,3 @@ export { toSpaceIdentifier, isValidSpaceIdentifier } from './space_identifier_utils'; export { SpaceValidator } from './validate_space'; - -export { getCreateBreadcrumbs, getEditBreadcrumbs, getListBreadcrumbs } from './breadcrumbs'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.test.ts b/x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.test.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.ts b/x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/space_identifier_utils.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/space_identifier_utils.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.test.ts b/x-pack/legacy/plugins/spaces/public/management/lib/validate_space.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.test.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/validate_space.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.ts b/x-pack/legacy/plugins/spaces/public/management/lib/validate_space.ts similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.ts rename to x-pack/legacy/plugins/spaces/public/management/lib/validate_space.ts index e7b91161314313..43d42dacdd36d7 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/validate_space.ts +++ b/x-pack/legacy/plugins/spaces/public/management/lib/validate_space.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { isReservedSpace } from '../../../../common/is_reserved_space'; -import { Space } from '../../../../common/model/space'; +import { isReservedSpace } from '../../../common/is_reserved_space'; +import { Space } from '../../../common/model/space'; import { isValidSpaceIdentifier } from './space_identifier_utils'; interface SpaceValidatorOptions { diff --git a/x-pack/legacy/plugins/spaces/public/management/management_service.test.ts b/x-pack/legacy/plugins/spaces/public/management/management_service.test.ts new file mode 100644 index 00000000000000..fa8ae641686737 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/management_service.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { ManagementService } from '.'; + +describe('ManagementService', () => { + describe('#start', () => { + it('registers the spaces management page under the kibana section', () => { + const mockKibanaSection = { + hasItem: jest.fn().mockReturnValue(false), + register: jest.fn(), + }; + + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(mockKibanaSection), + }, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + expect(deps.managementStart.legacy.getSection).toHaveBeenCalledTimes(1); + expect(deps.managementStart.legacy.getSection).toHaveBeenCalledWith('kibana'); + + expect(mockKibanaSection.register).toHaveBeenCalledTimes(1); + expect(mockKibanaSection.register).toHaveBeenCalledWith('spaces', { + name: 'spacesManagementLink', + order: 10, + display: 'Spaces', + url: `#/management/spaces/list`, + }); + }); + + it('will not register the spaces management page twice', () => { + const mockKibanaSection = { + hasItem: jest.fn().mockReturnValue(true), + register: jest.fn(), + }; + + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(mockKibanaSection), + }, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + expect(mockKibanaSection.register).toHaveBeenCalledTimes(0); + }); + + it('will not register the spaces management page if the kibana section is missing', () => { + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(undefined), + }, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + expect(deps.managementStart.legacy.getSection).toHaveBeenCalledTimes(1); + }); + }); + + describe('#stop', () => { + it('deregisters the spaces management page', () => { + const mockKibanaSection = { + hasItem: jest + .fn() + .mockReturnValueOnce(false) + .mockReturnValueOnce(true), + register: jest.fn(), + deregister: jest.fn(), + }; + + const managementStart = { + legacy: { + getSection: jest.fn().mockReturnValue(mockKibanaSection), + }, + }; + + const deps = { + managementStart, + }; + + const service = new ManagementService(); + service.start(deps); + + service.stop(); + + expect(mockKibanaSection.register).toHaveBeenCalledTimes(1); + expect(mockKibanaSection.deregister).toHaveBeenCalledTimes(1); + expect(mockKibanaSection.deregister).toHaveBeenCalledWith('spaces'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/spaces/public/management/management_service.ts b/x-pack/legacy/plugins/spaces/public/management/management_service.ts new file mode 100644 index 00000000000000..ada38f5cf3387e --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/management/management_service.ts @@ -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 { i18n } from '@kbn/i18n'; +import { ManagementStart } from 'src/plugins/management/public'; + +interface StartDeps { + managementStart: ManagementStart; +} + +const MANAGE_SPACES_KEY = 'spaces'; + +export class ManagementService { + private kibanaSection!: any; + + public start({ managementStart }: StartDeps) { + this.kibanaSection = managementStart.legacy.getSection('kibana'); + if (this.kibanaSection && !this.kibanaSection.hasItem(MANAGE_SPACES_KEY)) { + this.kibanaSection.register(MANAGE_SPACES_KEY, { + name: 'spacesManagementLink', + order: 10, + display: i18n.translate('xpack.spaces.displayName', { + defaultMessage: 'Spaces', + }), + url: `#/management/spaces/list`, + }); + } + } + + public stop() { + if (this.kibanaSection && this.kibanaSection.hasItem(MANAGE_SPACES_KEY)) { + this.kibanaSection.deregister(MANAGE_SPACES_KEY); + } + } +} diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/index.ts b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/index.ts rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 9fa03b1a9b74ae..6ca1877642bdc6 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -22,13 +22,13 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; import { Capabilities } from 'src/core/public'; -import { Feature } from '../../../../../../../plugins/features/public'; -import { isReservedSpace } from '../../../../common'; -import { DEFAULT_SPACE_ID } from '../../../../common/constants'; -import { Space } from '../../../../common/model/space'; -import { SpaceAvatar } from '../../../components'; -import { getSpacesFeatureDescription } from '../../../lib/constants'; -import { SpacesManager } from '../../../lib/spaces_manager'; +import { Feature } from '../../../../../../plugins/features/public'; +import { isReservedSpace } from '../../../common'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { Space } from '../../../common/model/space'; +import { SpaceAvatar } from '../../space_avatar'; +import { getSpacesFeatureDescription } from '../../constants'; +import { SpacesManager } from '../..//spaces_manager'; import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; import { SecureSpaceMessage } from '../components/secure_space_message'; import { UnauthorizedPrompt } from '../components/unauthorized_prompt'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx rename to x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx index 4add607707b248..7856d2e7bee01c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx @@ -6,12 +6,12 @@ jest.mock('ui/kfetch', () => ({ kfetch: () => Promise.resolve([]), })); -import '../../../__mocks__/xpack_info'; +import '../../__mocks__/xpack_info'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { SpaceAvatar } from '../../../components'; -import { spacesManagerMock } from '../../../lib/mocks'; -import { SpacesManager } from '../../../lib'; +import { SpaceAvatar } from '../../space_avatar'; +import { spacesManagerMock } from '../../spaces_manager/mocks'; +import { SpacesManager } from '../../spaces_manager'; import { SpacesGridPage } from './spaces_grid_page'; const spaces = [ diff --git a/x-pack/legacy/plugins/spaces/public/views/management/template.html b/x-pack/legacy/plugins/spaces/public/management/template.html similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/management/template.html rename to x-pack/legacy/plugins/spaces/public/management/template.html diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/_index.scss b/x-pack/legacy/plugins/spaces/public/nav_control/_index.scss similarity index 55% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/_index.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/_index.scss index 192091fb04e3c9..d0471da325cec9 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/_index.scss +++ b/x-pack/legacy/plugins/spaces/public/nav_control/_index.scss @@ -1 +1,2 @@ @import './components/index'; +@import './nav_control'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/_nav_control.scss b/x-pack/legacy/plugins/spaces/public/nav_control/_nav_control.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/_nav_control.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/_nav_control.scss diff --git a/x-pack/legacy/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/manage_spaces_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/__snapshots__/manage_spaces_button.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/manage_spaces_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/spaces_description.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/nav_control/components/__snapshots__/spaces_description.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/_index.scss b/x-pack/legacy/plugins/spaces/public/nav_control/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/_index.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/components/_index.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_description.scss b/x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_description.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_description.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_description.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_menu.scss b/x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_menu.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/_spaces_menu.scss rename to x-pack/legacy/plugins/spaces/public/nav_control/components/_spaces_menu.scss diff --git a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx index 91a0803c20bc9e..857d0c1f828a69 100644 --- a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx @@ -8,7 +8,7 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, CSSProperties } from 'react'; import { Capabilities } from 'src/core/public'; -import { getManageSpacesUrl } from '../lib/constants'; +import { getManageSpacesUrl } from '../../constants'; interface Props { isDisabled?: boolean; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.tsx similarity index 89% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.tsx index 043fc656a571e8..abf3c636b839ed 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_description.tsx @@ -7,8 +7,8 @@ import { EuiContextMenuPanel, EuiText } from '@elastic/eui'; import React, { FC } from 'react'; import { Capabilities } from 'src/core/public'; -import { ManageSpacesButton } from '../../../components'; -import { getSpacesFeatureDescription } from '../../../lib/constants'; +import { ManageSpacesButton } from './manage_spaces_button'; +import { getSpacesFeatureDescription } from '../../constants'; interface Props { onManageSpacesClick: () => void; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_menu.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_menu.tsx index 9a26f6802abdf5..96ce18896b426b 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/components/spaces_menu.tsx @@ -14,9 +14,10 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; import { Capabilities } from 'src/core/public'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants'; -import { Space } from '../../../../common/model/space'; -import { ManageSpacesButton, SpaceAvatar } from '../../../components'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common/constants'; +import { Space } from '../../../common/model/space'; +import { ManageSpacesButton } from './manage_spaces_button'; +import { SpaceAvatar } from '../../space_avatar'; interface Props { spaces: Space[]; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts b/x-pack/legacy/plugins/spaces/public/nav_control/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts rename to x-pack/legacy/plugins/spaces/public/nav_control/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control.tsx similarity index 94% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/nav_control.tsx index 0df077e0d2da04..9ec070eff3fed8 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SpacesManager } from 'plugins/spaces/lib/spaces_manager'; import React from 'react'; import ReactDOM from 'react-dom'; import { CoreStart } from 'src/core/public'; +import { SpacesManager } from '../spaces_manager'; import { NavControlPopover } from './nav_control_popover'; export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreStart) { diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.test.tsx similarity index 91% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.test.tsx index a04f28242f984a..5ce141abb713e3 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.test.tsx @@ -7,9 +7,9 @@ import * as Rx from 'rxjs'; import { shallow } from 'enzyme'; import React from 'react'; -import { SpaceAvatar } from '../../components'; -import { spacesManagerMock } from '../../lib/mocks'; -import { SpacesManager } from '../../lib'; +import { SpaceAvatar } from '../space_avatar'; +import { spacesManagerMock } from '../spaces_manager/mocks'; +import { SpacesManager } from '../spaces_manager'; import { NavControlPopover } from './nav_control_popover'; import { EuiHeaderSectionItemButton } from '@elastic/eui'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -42,6 +42,7 @@ describe('NavControlPopover', () => { disabledFeatures: [], }, ]); + // @ts-ignore readonly check spacesManager.onActiveSpaceChange$ = Rx.of({ id: 'foo-space', name: 'foo', diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.tsx similarity index 96% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.tsx index b37458aace2a28..f291027e15232c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx +++ b/x-pack/legacy/plugins/spaces/public/nav_control/nav_control_popover.tsx @@ -13,9 +13,9 @@ import { import React, { Component } from 'react'; import { Capabilities } from 'src/core/public'; import { Subscription } from 'rxjs'; -import { Space } from '../../../common/model/space'; -import { SpaceAvatar } from '../../components'; -import { SpacesManager } from '../../lib/spaces_manager'; +import { Space } from '../../common/model/space'; +import { SpaceAvatar } from '../space_avatar'; +import { SpacesManager } from '../spaces_manager'; import { SpacesDescription } from './components/spaces_description'; import { SpacesMenu } from './components/spaces_menu'; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/types.tsx b/x-pack/legacy/plugins/spaces/public/nav_control/types.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/nav_control/types.tsx rename to x-pack/legacy/plugins/spaces/public/nav_control/types.tsx diff --git a/x-pack/legacy/plugins/spaces/public/plugin.tsx b/x-pack/legacy/plugins/spaces/public/plugin.tsx index 4e070c3cee3df5..1ddb69a5b595cc 100644 --- a/x-pack/legacy/plugins/spaces/public/plugin.tsx +++ b/x-pack/legacy/plugins/spaces/public/plugin.tsx @@ -6,9 +6,14 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; -import { SpacesManager } from './lib'; -import { initSpacesNavControl } from './views/nav_control'; +import { ManagementSetup } from 'src/legacy/core_plugins/management/public'; +import { ManagementStart } from 'src/plugins/management/public'; +import { SpacesManager } from './spaces_manager'; +import { initSpacesNavControl } from './nav_control'; import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry'; +import { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space'; +import { AdvancedSettingsService } from './advanced_settings'; +import { ManagementService } from './management'; export interface SpacesPluginStart { spacesManager: SpacesManager | null; @@ -16,25 +21,61 @@ export interface SpacesPluginStart { export interface PluginsSetup { home?: HomePublicPluginSetup; + management: ManagementSetup; + __managementLegacyCompat: { + registerSettingsComponent: ( + id: string, + component: string | React.FC, + allowOverride: boolean + ) => void; + }; +} + +export interface PluginsStart { + management: ManagementStart; } export class SpacesPlugin implements Plugin { - private spacesManager: SpacesManager | null = null; + private spacesManager!: SpacesManager; - public async start(core: CoreStart) { - const serverBasePath = core.injectedMetadata.getInjectedVar('serverBasePath') as string; + private managementService?: ManagementService; + public setup(core: CoreSetup, plugins: PluginsSetup) { + const serverBasePath = core.injectedMetadata.getInjectedVar('serverBasePath') as string; this.spacesManager = new SpacesManager(serverBasePath, core.http); + + const copySavedObjectsToSpaceService = new CopySavedObjectsToSpaceService(); + copySavedObjectsToSpaceService.setup({ + spacesManager: this.spacesManager, + managementSetup: plugins.management, + }); + + const advancedSettingsService = new AdvancedSettingsService(); + advancedSettingsService.setup({ + getActiveSpace: () => this.spacesManager.getActiveSpace(), + registerSettingsComponent: plugins.__managementLegacyCompat.registerSettingsComponent, + }); + + if (plugins.home) { + plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); + } + } + + public start(core: CoreStart, plugins: PluginsStart) { initSpacesNavControl(this.spacesManager, core); + this.managementService = new ManagementService(); + this.managementService.start({ managementStart: plugins.management }); + return { spacesManager: this.spacesManager, }; } - public async setup(core: CoreSetup, plugins: PluginsSetup) { - if (plugins.home) { - plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); + public stop() { + if (this.managementService) { + this.managementService.stop(); + this.managementService = undefined; } } } diff --git a/x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/space_avatar/__snapshots__/space_avatar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/__snapshots__/space_avatar.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/space_avatar/__snapshots__/space_avatar.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/components/index.ts b/x-pack/legacy/plugins/spaces/public/space_avatar/index.ts similarity index 82% rename from x-pack/legacy/plugins/spaces/public/components/index.ts rename to x-pack/legacy/plugins/spaces/public/space_avatar/index.ts index 2e73f0c704f8c5..1525f2c8c61866 100644 --- a/x-pack/legacy/plugins/spaces/public/components/index.ts +++ b/x-pack/legacy/plugins/spaces/public/space_avatar/index.ts @@ -5,4 +5,4 @@ */ export { SpaceAvatar } from './space_avatar'; -export { ManageSpacesButton } from './manage_spaces_button'; +export * from './space_attributes'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts b/x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts b/x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_attributes.ts diff --git a/x-pack/legacy/plugins/spaces/public/components/space_avatar.test.tsx b/x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/components/space_avatar.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.tsx similarity index 98% rename from x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx rename to x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.tsx index 0d9751ca43db94..c89f492a8fc99f 100644 --- a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_avatar/space_avatar.tsx @@ -8,7 +8,7 @@ import { EuiAvatar, isValidHex } from '@elastic/eui'; import React, { FC } from 'react'; import { MAX_SPACE_INITIALS } from '../../common'; import { Space } from '../../common/model/space'; -import { getSpaceColor, getSpaceInitials, getSpaceImageUrl } from '../lib/space_attributes'; +import { getSpaceColor, getSpaceInitials, getSpaceImageUrl } from './space_attributes'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.tsx.snap rename to x-pack/legacy/plugins/spaces/public/space_selector/__snapshots__/space_selector.test.tsx.snap diff --git a/x-pack/legacy/plugins/spaces/public/space_selector/_index.scss b/x-pack/legacy/plugins/spaces/public/space_selector/_index.scss new file mode 100644 index 00000000000000..0621aa2a3efd72 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/space_selector/_index.scss @@ -0,0 +1,2 @@ +@import './space_selector'; +@import './components/index'; diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss b/x-pack/legacy/plugins/spaces/public/space_selector/_space_selector.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/_space_selector.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/_space_selector.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/_index.scss b/x-pack/legacy/plugins/spaces/public/space_selector/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/_index.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/components/_index.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/_space_card.scss b/x-pack/legacy/plugins/spaces/public/space_selector/components/_space_card.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/_space_card.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/components/_space_card.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/_space_cards.scss b/x-pack/legacy/plugins/spaces/public/space_selector/components/_space_cards.scss similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/_space_cards.scss rename to x-pack/legacy/plugins/spaces/public/space_selector/components/_space_cards.scss diff --git a/x-pack/legacy/plugins/spaces/public/views/components/index.ts b/x-pack/legacy/plugins/spaces/public/space_selector/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/index.ts rename to x-pack/legacy/plugins/spaces/public/space_selector/components/index.ts diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/space_card.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_card.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.tsx similarity index 90% rename from x-pack/legacy/plugins/spaces/public/views/components/space_card.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.tsx index 2386f6a6fe9d0e..f898ba87c60bd6 100644 --- a/x-pack/legacy/plugins/spaces/public/views/components/space_card.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_card.tsx @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - // FIXME: need updated typedefs - // @ts-ignore - EuiCard, -} from '@elastic/eui'; +import { EuiCard } from '@elastic/eui'; import React from 'react'; import { Space } from '../../../common/model/space'; -import { SpaceAvatar } from '../../components'; +import { SpaceAvatar } from '../../space_avatar'; interface Props { space: Space; diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_cards.test.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.test.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/space_cards.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.test.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/components/space_cards.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.tsx similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/components/space_cards.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/components/space_cards.tsx diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/index.tsx similarity index 82% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/index.tsx index c520c2683c9651..c1c1b6dc3a2f35 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/index.tsx @@ -5,7 +5,7 @@ */ // @ts-ignore -import template from 'plugins/spaces/views/space_selector/space_selector.html'; +import template from 'plugins/spaces/space_selector/space_selector.html'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; // @ts-ignore @@ -15,14 +15,14 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { SpaceSelector } from './space_selector'; -import { start as spacesNPStart } from '../../legacy'; +import { start as spacesNPStart } from '../legacy'; const module = uiModules.get('spaces_selector', []); module.controller('spacesSelectorController', ($scope: any) => { - $scope.$$postDigest(async () => { + $scope.$$postDigest(() => { const domNode = document.getElementById('spaceSelectorRoot'); - const { spacesManager } = await spacesNPStart; + const { spacesManager } = spacesNPStart; render( diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.html b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.html similarity index 100% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.html rename to x-pack/legacy/plugins/spaces/public/space_selector/space_selector.html diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.test.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.test.tsx similarity index 92% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.test.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/space_selector.test.tsx index 829312061ca980..b4d0f96307500a 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Space } from '../../../common/model/space'; -import { spacesManagerMock } from '../../lib/mocks'; +import { Space } from '../../common/model/space'; +import { spacesManagerMock } from '../spaces_manager/mocks'; import { SpaceSelector } from './space_selector'; function getSpacesManager(spaces: Space[] = []) { diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.tsx b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.tsx similarity index 95% rename from x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.tsx rename to x-pack/legacy/plugins/spaces/public/space_selector/space_selector.tsx index d665752b3c8a65..206d38454fa8c5 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/space_selector.tsx +++ b/x-pack/legacy/plugins/spaces/public/space_selector/space_selector.tsx @@ -19,11 +19,11 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { SpacesManager } from 'plugins/spaces/lib'; import React, { Component, Fragment } from 'react'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common/constants'; -import { Space } from '../../../common/model/space'; -import { SpaceCards } from '../components/space_cards'; +import { SpacesManager } from '../spaces_manager'; +import { Space } from '../../common/model/space'; +import { SpaceCards } from './components'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../common/constants'; interface Props { spacesManager: SpacesManager; diff --git a/x-pack/legacy/plugins/spaces/public/lib/index.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/index.ts similarity index 76% rename from x-pack/legacy/plugins/spaces/public/lib/index.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/index.ts index 56ac7b8ff37f4f..538dd77e053f57 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/index.ts +++ b/x-pack/legacy/plugins/spaces/public/spaces_manager/index.ts @@ -5,4 +5,3 @@ */ export { SpacesManager } from './spaces_manager'; -export { getSpaceInitials, getSpaceColor, getSpaceImageUrl } from './space_attributes'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/mocks.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/mocks.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/mocks.ts diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts similarity index 87% rename from x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index 69c6f7a452fdd0..56879af33916fc 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts +++ b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -6,9 +6,10 @@ import { of, Observable } from 'rxjs'; import { Space } from '../../common/model/space'; +import { SpacesManager } from './spaces_manager'; function createSpacesManagerMock() { - return { + return ({ onActiveSpaceChange$: (of(undefined) as unknown) as Observable, getSpaces: jest.fn().mockResolvedValue([]), getSpace: jest.fn().mockResolvedValue(undefined), @@ -19,7 +20,8 @@ function createSpacesManagerMock() { copySavedObjects: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), - }; + changeSelectedSpace: jest.fn(), + } as unknown) as jest.Mocked; } export const spacesManagerMock = { diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.test.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/public/lib/spaces_manager.test.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.test.ts diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.ts similarity index 97% rename from x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts rename to x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.ts index ccc1b00dabb293..e9c738cf40c69a 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/legacy/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -9,9 +9,9 @@ import { HttpSetup } from 'src/core/public'; import { SavedObjectsManagementRecord } from '../../../../../../src/legacy/core_plugins/management/public'; import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; -import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/types'; import { ENTER_SPACE_PATH } from '../../common/constants'; import { addSpaceIdToPath } from '../../../../../plugins/spaces/common'; +import { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types'; export class SpacesManager { private activeSpace$: BehaviorSubject = new BehaviorSubject(null); diff --git a/x-pack/legacy/plugins/spaces/public/views/_index.scss b/x-pack/legacy/plugins/spaces/public/views/_index.scss deleted file mode 100644 index 0cc8ccb10246b0..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './components/index'; -@import './management/index'; -@import './nav_control/index'; -@import './space_selector/index' diff --git a/x-pack/legacy/plugins/spaces/public/views/management/_index.scss b/x-pack/legacy/plugins/spaces/public/views/management/_index.scss deleted file mode 100644 index e7cbdfe2de7e81..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './components/confirm_delete_modal'; -@import './edit_space/enabled_features/index'; -@import './components/copy_saved_objects_to_space/index'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx deleted file mode 100644 index bf33273c614d65..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx +++ /dev/null @@ -1,77 +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 { i18n } from '@kbn/i18n'; -import 'plugins/spaces/views/management/page_routes'; -import React from 'react'; -import { - management, - PAGE_SUBTITLE_COMPONENT, - PAGE_TITLE_COMPONENT, - registerSettingsComponent, -} from 'ui/management'; -// @ts-ignore -import routes from 'ui/routes'; -import { setup as managementSetup } from '../../../../../../../src/legacy/core_plugins/management/public/legacy'; -import { AdvancedSettingsSubtitle } from './components/advanced_settings_subtitle'; -import { AdvancedSettingsTitle } from './components/advanced_settings_title'; -import { start as spacesNPStart } from '../../legacy'; -import { CopyToSpaceSavedObjectsManagementAction } from '../../lib/copy_saved_objects_to_space'; - -const MANAGE_SPACES_KEY = 'spaces'; - -routes.defaults(/\/management/, { - resolve: { - spacesManagementSection() { - function getKibanaSection() { - return management.getSection('kibana'); - } - - function deregisterSpaces() { - getKibanaSection().deregister(MANAGE_SPACES_KEY); - } - - function ensureSpagesRegistered() { - const kibanaSection = getKibanaSection(); - - if (!kibanaSection.hasItem(MANAGE_SPACES_KEY)) { - kibanaSection.register(MANAGE_SPACES_KEY, { - name: 'spacesManagementLink', - order: 10, - display: i18n.translate('xpack.spaces.displayName', { - defaultMessage: 'Spaces', - }), - url: `#/management/spaces/list`, - }); - } - - // Customize Saved Objects Management - spacesNPStart.then(({ spacesManager }) => { - const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager!); - // This route resolve function executes any time the management screen is loaded, and we want to ensure - // that this action is only registered once. - if (!managementSetup.savedObjects.registry.has(action.id)) { - managementSetup.savedObjects.registry.register(action); - } - }); - - const getActiveSpace = async () => { - const { spacesManager } = await spacesNPStart; - return spacesManager!.getActiveSpace(); - }; - - const PageTitle = () => ; - registerSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle, true); - - const SubTitle = () => ; - registerSettingsComponent(PAGE_SUBTITLE_COMPONENT, SubTitle, true); - } - - deregisterSpaces(); - - ensureSpagesRegistered(); - }, - }, -}); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/breadcrumbs.ts b/x-pack/legacy/plugins/spaces/public/views/management/lib/breadcrumbs.ts deleted file mode 100644 index a4e8ba508b6177..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/breadcrumbs.ts +++ /dev/null @@ -1,36 +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 { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; -import { Space } from '../../../../common/model/space'; - -export function getListBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: 'Spaces', - href: '#/management/spaces/list', - }, - ]; -} - -export function getCreateBreadcrumbs() { - return [ - ...getListBreadcrumbs(), - { - text: 'Create', - }, - ]; -} - -export function getEditBreadcrumbs(space?: Space) { - return [ - ...getListBreadcrumbs(), - { - text: space ? space.name : '...', - }, - ]; -} diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/_index.scss b/x-pack/legacy/plugins/spaces/public/views/space_selector/_index.scss deleted file mode 100644 index f23ac662dce1dc..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './space_selector'; From 8751e7cf89256ce00c80ad58947a6de6d969bb18 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 6 Jan 2020 19:29:26 +0000 Subject: [PATCH 005/137] [Visualization] Add default filename when exporting if no filename provided (#54003) --- .../public/views/data/components/download_options.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/inspector/public/views/data/components/download_options.tsx b/src/plugins/inspector/public/views/data/components/download_options.tsx index 6d21dcdafa84d5..e7bfbed23c0746 100644 --- a/src/plugins/inspector/public/views/data/components/download_options.tsx +++ b/src/plugins/inspector/public/views/data/components/download_options.tsx @@ -20,6 +20,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { DataViewColumn, DataViewRow } from '../types'; @@ -66,8 +67,14 @@ class DataDownloadOptions extends Component { + let filename = this.props.title; + if (!filename || filename.length === 0) { + filename = i18n.translate('inspector.data.downloadOptionsUnsavedFilename', { + defaultMessage: 'unsaved', + }); + } exportAsCsv({ - filename: `${this.props.title}.csv`, + filename: `${filename}.csv`, columns: this.props.columns, rows: this.props.rows, csvSeparator: this.props.csvSeparator, From ae8925c1239fe1538c50fc6f6abfbf58a7ffa7e2 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Mon, 6 Jan 2020 14:54:49 -0500 Subject: [PATCH 006/137] Select Records when count operation is chosen (#53911) --- .../dimension_panel/dimension_panel.test.tsx | 11 ++++------- .../dimension_panel/popover_editor.tsx | 7 +------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 82e172b6bd7e27..626ef99ac13aae 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -610,7 +610,7 @@ describe('IndexPatternDimensionPanel', () => { }); }); - it('should indicate document compatibility with selected field operation', () => { + it('should select the Records field when count is selected', () => { const initialState: IndexPatternPrivateState = { ...state, layers: { @@ -639,12 +639,9 @@ describe('IndexPatternDimensionPanel', () => { .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') .simulate('click'); - const options = wrapper.find(EuiComboBox).prop('options'); - - expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - options![1].options!.map(option => - expect(option['data-test-subj']).toContain('Incompatible') - ); + const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; + expect(newColumnState.operationType).toEqual('count'); + expect(newColumnState.sourceField).toEqual('Records'); }); it('should indicate document and field compatibility with selected document operation', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index c44d63b01c1b3f..98773c04db4a6a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -127,7 +127,7 @@ export function PopoverEditor(props: PopoverEditorProps) { compatibleWithCurrentField ? '' : 'Incompatible' }-${operationType}`, onClick() { - if (!selectedColumn) { + if (!selectedColumn || !compatibleWithCurrentField) { const possibleFields = fieldByOperation[operationType] || []; if (possibleFields.length === 1) { @@ -152,11 +152,6 @@ export function PopoverEditor(props: PopoverEditorProps) { trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } - if (!compatibleWithCurrentField) { - setInvalidOperationType(operationType); - trackUiEvent(`indexpattern_dimension_operation_${operationType}`); - return; - } if (incompatibleSelectedOperationType) { setInvalidOperationType(null); } From dbe0bfdf7936b25061b6e8b8c52a122c2d73eeb7 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 6 Jan 2020 14:55:12 -0500 Subject: [PATCH 007/137] [Maps] Allow editing EMS basemap selection (#53631) * [Maps] Allow editing EMS basemap selection * resync source data when map changes * remove old sources and layers * update ownsMbLayerId and ownsMbSourceId so they return true for previous sources when source changes * review feedback Co-authored-by: Elastic Machine --- .../sources/ems_tms_source/ems_tms_source.js | 17 ++-- ...ource_editor.js => tile_service_select.js} | 80 ++++++++++--------- .../ems_tms_source/update_source_editor.js | 38 +++++++++ .../maps/public/layers/vector_tile_layer.js | 76 ++++++++++++++---- 4 files changed, 153 insertions(+), 58 deletions(-) rename x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/{create_source_editor.js => tile_service_select.js} (52%) create mode 100644 x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/update_source_editor.js diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 8850c4c07ab736..76ecc18f2f7d76 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -11,7 +11,8 @@ import { AbstractTMSSource } from '../tms_source'; import { VectorTileLayer } from '../../vector_tile_layer'; import { getEMSClient } from '../../../meta'; -import { EMSTMSCreateSourceEditor } from './create_source_editor'; +import { TileServiceSelect } from './tile_service_select'; +import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { EMS_TMS } from '../../../../common/constants'; @@ -41,7 +42,7 @@ export class EMSTMSSource extends AbstractTMSSource { onPreviewSource(source); }; - return ; + return ; } constructor(descriptor, inspectorAdapters) { @@ -55,6 +56,10 @@ export class EMSTMSSource extends AbstractTMSSource { ); } + renderSourceSettingsEditor({ onChange }) { + return ; + } + async getImmutableProperties() { const displayName = await this.getDisplayName(); const autoSelectMsg = i18n.translate('xpack.maps.source.emsTile.isAutoSelectLabel', { @@ -78,7 +83,7 @@ export class EMSTMSSource extends AbstractTMSSource { async _getEMSTMSService() { const emsClient = getEMSClient(); const emsTMSServices = await emsClient.getTMSServices(); - const emsTileLayerId = this._getEmsTileLayerId(); + const emsTileLayerId = this.getTileLayerId(); const tmsService = emsTMSServices.find(tmsService => tmsService.getId() === emsTileLayerId); if (!tmsService) { throw new Error( @@ -110,7 +115,7 @@ export class EMSTMSSource extends AbstractTMSSource { const emsTMSService = await this._getEMSTMSService(); return emsTMSService.getDisplayName(); } catch (error) { - return this._getEmsTileLayerId(); + return this.getTileLayerId(); } } @@ -129,7 +134,7 @@ export class EMSTMSSource extends AbstractTMSSource { } getSpriteNamespacePrefix() { - return 'ems/' + this._getEmsTileLayerId(); + return 'ems/' + this.getTileLayerId(); } async getVectorStyleSheetAndSpriteMeta(isRetina) { @@ -142,7 +147,7 @@ export class EMSTMSSource extends AbstractTMSSource { }; } - _getEmsTileLayerId() { + getTileLayerId() { if (!this._descriptor.isAutoSelect) { return this._descriptor.id; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/tile_service_select.js similarity index 52% rename from x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/create_source_editor.js rename to x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/tile_service_select.js index 65986d5bc93dfd..337fc7aa466931 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/tile_service_select.js @@ -13,74 +13,80 @@ import { i18n } from '@kbn/i18n'; export const AUTO_SELECT = 'auto_select'; -export class EMSTMSCreateSourceEditor extends React.Component { +export class TileServiceSelect extends React.Component { state = { - emsTmsOptionsRaw: null, + emsTmsOptions: [], + hasLoaded: false, }; + componentWillUnmount() { + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + this._loadTmsOptions(); + } + _loadTmsOptions = async () => { const emsClient = getEMSClient(); const emsTMSServices = await emsClient.getTMSServices(); - const options = emsTMSServices.map(tmsService => { + + if (!this._isMounted) { + return; + } + + const emsTmsOptions = emsTMSServices.map(tmsService => { return { - id: tmsService.getId(), - name: tmsService.getDisplayName(), + value: tmsService.getId(), + text: tmsService.getDisplayName() ? tmsService.getDisplayName() : tmsService.getId(), }; }); - options.unshift({ - id: AUTO_SELECT, - name: i18n.translate('xpack.maps.source.emsTile.autoLabel', { + emsTmsOptions.unshift({ + value: AUTO_SELECT, + text: i18n.translate('xpack.maps.source.emsTile.autoLabel', { defaultMessage: 'Autoselect based on Kibana theme', }), }); - if (this._isMounted) { - this.setState({ - emsTmsOptionsRaw: options, - }); - } + this.setState({ emsTmsOptions, hasLoaded: true }); }; - _onEmsTileServiceChange = e => { + _onChange = e => { const value = e.target.value; const isAutoSelect = value === AUTO_SELECT; - this.props.onSourceConfigChange({ + this.props.onTileSelect({ id: isAutoSelect ? null : value, isAutoSelect, }); }; - componentWillUnmount() { - this._isMounted = false; - } - - componentDidMount() { - this._isMounted = true; - this._loadTmsOptions(); - } - render() { - if (!this.state.emsTmsOptionsRaw) { - // TODO display loading message - return null; - } + const helpText = + this.state.hasLoaded && this.state.emsTmsOptions.length === 0 + ? getEmsUnavailableMessage() + : null; - const emsTileOptions = this.state.emsTmsOptionsRaw.map(service => ({ - value: service.id, - text: service.name || service.id, - })); + let selectedId; + if (this.props.config) { + selectedId = this.props.config.isAutoSelect ? AUTO_SELECT : this.props.config.id; + } return ( ); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/update_source_editor.js new file mode 100644 index 00000000000000..4d567b8dbb32a9 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/update_source_editor.js @@ -0,0 +1,38 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TileServiceSelect } from './tile_service_select'; + +export function UpdateSourceEditor({ onChange, config }) { + const _onTileSelect = ({ id, isAutoSelect }) => { + onChange({ propName: 'id', value: id }); + onChange({ propName: 'isAutoSelect', value: isAutoSelect }); + }; + + return ( + + + +
+ +
+
+ + + + +
+ + +
+ ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js index c1b590f56ae526..b09ccdc3af8bae 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js @@ -31,32 +31,58 @@ export class VectorTileLayer extends TileLayer { return tileLayerDescriptor; } + _canSkipSync({ prevDataRequest, nextMeta }) { + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + return prevMeta.tileLayerId === nextMeta.tileLayerId; + } + async syncData({ startLoading, stopLoading, onLoadError, dataFilters }) { if (!this.isVisible() || !this.showAtZoomLevel(dataFilters.zoom)) { return; } - const sourceDataRequest = this.getSourceDataRequest(); - if (sourceDataRequest) { - //data is immmutable + + const nextMeta = { tileLayerId: this._source.getTileLayerId() }; + const canSkipSync = this._canSkipSync({ + prevDataRequest: this.getSourceDataRequest(), + nextMeta, + }); + if (canSkipSync) { return; } + const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); - startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); try { + startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); const styleAndSprites = await this._source.getVectorStyleSheetAndSpriteMeta(isRetina()); const spriteSheetImageData = await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png); const data = { ...styleAndSprites, spriteSheetImageData, }; - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, data, {}); + stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, data, nextMeta); } catch (error) { onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); } } _generateMbId(name) { - return this.getId() + '_' + name; + return `${this.getId()}_${name}`; + } + + _generateMbSourceIdPrefix() { + const DELIMITTER = '___'; + return `${this.getId()}${DELIMITTER}${this._source.getTileLayerId()}${DELIMITTER}`; + } + + _generateMbSourceId(name) { + return `${this._generateMbSourceIdPrefix()}${name}`; } _getVectorStyle() { @@ -103,19 +129,15 @@ export class VectorTileLayer extends TileLayer { return []; } const sourceIds = Object.keys(vectorStyle.sources); - return sourceIds.map(sourceId => this._generateMbId(sourceId)); + return sourceIds.map(sourceId => this._generateMbSourceId(sourceId)); } ownsMbLayerId(mbLayerId) { - //todo optimize: do not create temp array - const mbLayerIds = this.getMbLayerIds(); - return mbLayerIds.indexOf(mbLayerId) >= 0; + return mbLayerId.startsWith(this.getId()); } ownsMbSourceId(mbSourceId) { - //todo optimize: do not create temp array - const mbSourceIds = this.getMbSourceIds(); - return mbSourceIds.indexOf(mbSourceId) >= 0; + return mbSourceId.startsWith(this.getId()); } _makeNamespacedImageId(imageId) { @@ -123,19 +145,43 @@ export class VectorTileLayer extends TileLayer { return prefix + imageId; } + _requiresPrevSourceCleanup(mbMap) { + const sourceIdPrefix = this._generateMbSourceIdPrefix(); + const mbStyle = mbMap.getStyle(); + return Object.keys(mbStyle.sources).some(mbSourceId => { + const doesMbSourceBelongToLayer = this.ownsMbSourceId(mbSourceId); + const doesMbSourceBelongToSource = mbSourceId.startsWith(sourceIdPrefix); + return doesMbSourceBelongToLayer && !doesMbSourceBelongToSource; + }); + } + syncLayerWithMB(mbMap) { const vectorStyle = this._getVectorStyle(); if (!vectorStyle) { return; } + if (this._requiresPrevSourceCleanup(mbMap)) { + const mbStyle = mbMap.getStyle(); + mbStyle.layers.forEach(mbLayer => { + if (this.ownsMbLayerId(mbLayer.id)) { + mbMap.removeLayer(mbLayer.id); + } + }); + Object.keys(mbStyle.sources).some(mbSourceId => { + if (this.ownsMbSourceId(mbSourceId)) { + mbMap.removeSource(mbSourceId); + } + }); + } + let initialBootstrapCompleted = false; const sourceIds = Object.keys(vectorStyle.sources); sourceIds.forEach(sourceId => { if (initialBootstrapCompleted) { return; } - const mbSourceId = this._generateMbId(sourceId); + const mbSourceId = this._generateMbSourceId(sourceId); const mbSource = mbMap.getSource(mbSourceId); if (mbSource) { //if a single source is present, the layer already has bootstrapped with the mbMap @@ -174,7 +220,7 @@ export class VectorTileLayer extends TileLayer { } const newLayerObject = { ...layer, - source: this._generateMbId(layer.source), + source: this._generateMbSourceId(layer.source), id: mbLayerId, }; From f08fc201c8bc70b6e10b45fbc4c7aa3f3f1141b6 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Mon, 6 Jan 2020 14:55:51 -0500 Subject: [PATCH 008/137] Add support for scripted fields and (#53948) default index pattern Co-authored-by: Elastic Machine --- .../indexpattern_plugin/indexpattern.tsx | 8 ++++-- .../public/indexpattern_plugin/loader.test.ts | 27 ++++++++++++++++--- .../lens/public/indexpattern_plugin/loader.ts | 6 +++-- .../lens/public/indexpattern_plugin/types.ts | 1 + 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 15f19bb9d97e6b..b58a2d8ca52c75 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -115,8 +115,12 @@ export function getIndexPatternDatasource({ const indexPatternDatasource: Datasource = { id: 'indexpattern', - initialize(state?: IndexPatternPersistedState) { - return loadInitialState({ state, savedObjectsClient }); + async initialize(state?: IndexPatternPersistedState) { + return loadInitialState({ + state, + savedObjectsClient, + defaultIndexPatternId: core.uiSettings.get('defaultIndex'), + }); }, getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts index 2fb678aed5a54b..e180ab690d4185 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts @@ -114,8 +114,9 @@ const sampleIndexPatterns = { { name: 'source', type: 'string', - aggregatable: true, - searchable: true, + aggregatable: false, + searchable: false, + scripted: true, aggregationRestrictions: { terms: { agg: 'terms', @@ -196,7 +197,7 @@ describe('loader', () => { expect(cache).toMatchObject(sampleIndexPatterns); }); - it('should not allow full text fields', async () => { + it('should allow scripted, but not full text fields', async () => { const cache = await loadIndexPatterns({ cache: {}, patterns: ['a', 'b'], @@ -286,6 +287,26 @@ describe('loader', () => { }); }); + it('should use the default index pattern id, if provided', async () => { + const state = await loadInitialState({ + defaultIndexPatternId: 'b', + savedObjectsClient: mockClient(), + }); + + expect(state).toMatchObject({ + currentIndexPatternId: 'b', + indexPatternRefs: [ + { id: 'a', title: sampleIndexPatterns.a.title }, + { id: 'b', title: sampleIndexPatterns.b.title }, + ], + indexPatterns: { + b: sampleIndexPatterns.b, + }, + layers: {}, + showEmptyFields: false, + }); + }); + it('should initialize from saved state', async () => { const savedState: IndexPatternPersistedState = { currentIndexPatternId: 'b', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts index 661c627f3454f6..7f46f50786cf48 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts @@ -84,9 +84,11 @@ export async function loadIndexPatterns({ export async function loadInitialState({ state, savedObjectsClient, + defaultIndexPatternId, }: { state?: IndexPatternPersistedState; savedObjectsClient: SavedObjectsClient; + defaultIndexPatternId?: string; }): Promise { const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); const requiredPatterns = _.unique( @@ -94,7 +96,7 @@ export async function loadInitialState({ ? Object.values(state.layers) .map(l => l.indexPatternId) .concat(state.currentIndexPatternId) - : [indexPatternRefs[0].id] + : [defaultIndexPatternId || indexPatternRefs[0].id] ); const currentIndexPatternId = requiredPatterns[0]; @@ -280,7 +282,7 @@ function fromSavedObject( type, title: attributes.title, fields: (JSON.parse(attributes.fields) as IndexPatternField[]) - .filter(({ aggregatable }) => !!aggregatable) + .filter(({ aggregatable, scripted }) => !!aggregatable || !!scripted) .concat(documentField), typeMeta: attributes.typeMeta ? (JSON.parse(attributes.typeMeta) as SavedRestrictionsInfo) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts index 9ed50836333148..50478515d19ce2 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts @@ -39,6 +39,7 @@ export interface IndexPatternField { type: string; esTypes?: string[]; aggregatable: boolean; + scripted?: boolean; searchable: boolean; aggregationRestrictions?: AggregationRestrictions; } From 368a894bd2c1600755ea4f85464d8ddb0b704d5c Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 6 Jan 2020 13:05:07 -0700 Subject: [PATCH 009/137] [Maps] Fix regression preventing maps telemetry from populating & remove task manager logic (#52834) * Remove task logic. Remove server refs and revise for np. Migrate a few files to ts * Remove unused reference * Update mappings * Test usage collector register * Update api integration tests to include maps now that telemetry is 'normal' (not using task mgr state) * Update integration test to use stack stats * Update integration test to look for 'maps-telemetry' instead of 'maps' * Update jest test to reflect calls to register * Follow the same pattern as other int tests and test reliable nested attribute * Back out np-related changes for separate PR * timeCaptured hasn't changed but for some reason stopped working. Getting iso string fixes issue * Back out file shuffling for separate PR * Remove mappings updates (handled in separate PR) * Review feedback. Move telemetry type constant to constants file * Consolidate imports * Linting fix Co-authored-by: Elastic Machine --- .../legacy/plugins/maps/common/constants.js | 1 + .../server/maps_telemetry/maps_telemetry.js | 20 ++- .../maps_telemetry/maps_usage_collector.js | 84 ++---------- .../maps_usage_collector.test.js | 66 +++------ .../server/maps_telemetry/telemetry_task.js | 125 ------------------ .../maps_telemetry/telemetry_task.test.js | 68 ---------- .../apis/telemetry/telemetry_local.js | 4 + 7 files changed, 46 insertions(+), 322 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js delete mode 100644 x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index b97845a458d51b..6e7776d43f4d46 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -23,6 +23,7 @@ export const EMS_TILES_VECTOR_TILE_PATH = 'ems/tiles/vector/tile'; export const MAP_SAVED_OBJECT_TYPE = 'map'; export const APP_ID = 'maps'; export const APP_ICON = 'gisApp'; +export const TELEMETRY_TYPE = 'maps-telemetry'; export const MAP_APP_PATH = `app/${APP_ID}`; export const GIS_API_PATH = `api/${APP_ID}`; diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js index 6d078ae35ef850..848c964f4b6d4a 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js @@ -5,10 +5,16 @@ */ import _ from 'lodash'; -import { EMS_FILE, ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; +import { + EMS_FILE, + ES_GEO_FIELD_TYPE, + MAP_SAVED_OBJECT_TYPE, + TELEMETRY_TYPE, +} from '../../common/constants'; -function getSavedObjectsClient(server, callCluster) { +function getSavedObjectsClient(server) { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; + const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; const internalRepository = getSavedObjectsRepository(callCluster); return new SavedObjectsClient(internalRepository); } @@ -79,7 +85,7 @@ export function buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, // Total count of maps mapsTotalCount: mapsCount, // Time of capture - timeCaptured: new Date(), + timeCaptured: new Date().toISOString(), attributesPerMap: { // Count of data sources per map dataSourcesCount: { @@ -115,16 +121,16 @@ async function getIndexPatternSavedObjects(savedObjectsClient) { return _.get(indexPatternSavedObjects, 'saved_objects', []); } -export async function getMapsTelemetry(server, callCluster) { - const savedObjectsClient = getSavedObjectsClient(server, callCluster); +export async function getMapsTelemetry(server) { + const savedObjectsClient = getSavedObjectsClient(server); const mapSavedObjects = await getMapSavedObjects(savedObjectsClient); const indexPatternSavedObjects = await getIndexPatternSavedObjects(savedObjectsClient); const settings = { showMapVisualizationTypes: server.config().get('xpack.maps.showMapVisualizationTypes'), }; const mapsTelemetry = buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings }); - return await savedObjectsClient.create('maps-telemetry', mapsTelemetry, { - id: 'maps-telemetry', + return await savedObjectsClient.create(TELEMETRY_TYPE, mapsTelemetry, { + id: TELEMETRY_TYPE, overwrite: true, }); } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js index d1011736e77f8d..9c575e66f75567 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js @@ -4,85 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; -import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_task'; +import { getMapsTelemetry } from './maps_telemetry'; +import { TELEMETRY_TYPE } from '../../common/constants'; export function initTelemetryCollection(usageCollection, server) { - registerMapsTelemetryTask(server); - scheduleTask(server); - registerMapsUsageCollector(usageCollection, server); -} - -async function isTaskManagerReady(server) { - const result = await fetch(server); - return result !== null; -} - -async function fetch(server) { - let docs; - const taskManager = server.plugins.task_manager; - - if (!taskManager) { - return null; - } - - try { - ({ docs } = await taskManager.fetch({ - query: { - bool: { - filter: { - term: { - _id: `task:${TASK_ID}`, - }, - }, - }, - }, - })); - } catch (err) { - const errMessage = err && err.message ? err.message : err.toString(); - /* - * The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager - * has to wait for all plugins to initialize first. - * It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error) - */ - if (errMessage.indexOf('NotInitialized') >= 0) { - return null; - } else { - throw err; - } + if (!usageCollection) { + return; } - return docs; -} - -export function buildCollectorObj(server) { - let isCollectorReady = false; - async function determineIfTaskManagerIsReady() { - let isReady = false; - try { - isReady = await isTaskManagerReady(server); - } catch (err) {} // eslint-disable-line - - if (isReady) { - isCollectorReady = true; - } else { - setTimeout(determineIfTaskManagerIsReady, 500); - } - } - determineIfTaskManagerIsReady(); - - return { - type: 'maps', - isReady: () => isCollectorReady, - fetch: async () => { - const docs = await fetch(server); - return _.get(docs, '[0].state.stats'); - }, - }; -} + const mapsUsageCollector = usageCollection.makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => await getMapsTelemetry(server), + }); -export function registerMapsUsageCollector(usageCollection, server) { - const collectorObj = buildCollectorObj(server); - const mapsUsageCollector = usageCollection.makeUsageCollector(collectorObj); usageCollection.registerCollector(mapsUsageCollector); } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.test.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.test.js index 727d60b5088aaf..c5a3fca89b5608 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.test.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.test.js @@ -4,60 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; -import { getMockCallWithInternal, getMockKbnServer, getMockTaskFetch } from '../test_utils'; -import { buildCollectorObj } from './maps_usage_collector'; +import { initTelemetryCollection } from './maps_usage_collector'; describe('buildCollectorObj#fetch', () => { - let mockKbnServer; + let makeUsageCollectorStub; + let registerStub; + let usageCollection; beforeEach(() => { - mockKbnServer = getMockKbnServer(); + makeUsageCollectorStub = jest.fn(); + registerStub = jest.fn(); + usageCollection = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + }; }); - test('can return empty stats', async () => { - const { type, fetch } = buildCollectorObj(mockKbnServer); - expect(type).toBe('maps'); - const fetchResult = await fetch(); - expect(fetchResult).toEqual({}); - }); - - test('provides known stats', async () => { - const mockTaskFetch = getMockTaskFetch([ - { - state: { - runs: 2, - stats: { wombat_sightings: { total: 712, max: 84, min: 7, avg: 63 } }, - }, - }, - ]); - mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch); - - const { type, fetch } = buildCollectorObj(mockKbnServer); - expect(type).toBe('maps'); - const fetchResult = await fetch(); - expect(fetchResult).toEqual({ wombat_sightings: { total: 712, max: 84, min: 7, avg: 63 } }); - }); - - describe('Error handling', () => { - test('Silently handles Task Manager NotInitialized', async () => { - const mockTaskFetch = sinon.stub(); - mockTaskFetch.rejects( - new Error('NotInitialized taskManager is still waiting for plugins to load') - ); - mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch); - - const { fetch } = buildCollectorObj(mockKbnServer); - await expect(fetch()).resolves.toBe(undefined); - }); - // In real life, the CollectorSet calls fetch and handles errors - test('defers the errors', async () => { - const mockTaskFetch = sinon.stub(); - mockTaskFetch.rejects(new Error('Sad violin')); - mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch); + test('makes and registers maps usage collector', async () => { + const serverPlaceholder = {}; + initTelemetryCollection(usageCollection, serverPlaceholder); - const { fetch } = buildCollectorObj(mockKbnServer); - await expect(fetch()).rejects.toMatchObject(new Error('Sad violin')); + expect(registerStub).toHaveBeenCalledTimes(1); + expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); + expect(makeUsageCollectorStub).toHaveBeenCalledWith({ + type: expect.any(String), + isReady: expect.any(Function), + fetch: expect.any(Function), }); }); }); diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js deleted file mode 100644 index db5df358abc39f..00000000000000 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js +++ /dev/null @@ -1,125 +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 { getMapsTelemetry } from './maps_telemetry'; - -const TELEMETRY_TASK_TYPE = 'maps_telemetry'; - -export const TASK_ID = `Maps-${TELEMETRY_TASK_TYPE}`; - -export function scheduleTask(server) { - const taskManager = server.plugins.task_manager; - - if (!taskManager) { - server.log(['debug', 'telemetry'], `Task manager is not available`); - return; - } - - const { kbnServer } = server.plugins.xpack_main.status.plugin; - - kbnServer.afterPluginsInit(() => { - // The code block below can't await directly within "afterPluginsInit" - // callback due to circular dependency. The server isn't "ready" until - // this code block finishes. Migrations wait for server to be ready before - // executing. Saved objects repository waits for migrations to finish before - // finishing the request. To avoid this, we'll await within a separate - // function block. - (async () => { - try { - await taskManager.ensureScheduled({ - id: TASK_ID, - taskType: TELEMETRY_TASK_TYPE, - state: { stats: {}, runs: 0 }, - }); - } catch (e) { - server.log(['warning', 'maps'], `Error scheduling telemetry task, received ${e.message}`); - } - })(); - }); -} - -export function registerMapsTelemetryTask(server) { - const taskManager = server.plugins.task_manager; - - if (!taskManager) { - server.log(['debug', 'telemetry'], `Task manager is not available`); - return; - } - - taskManager.registerTaskDefinitions({ - [TELEMETRY_TASK_TYPE]: { - title: 'Maps telemetry fetch task', - type: TELEMETRY_TASK_TYPE, - timeout: '1m', - createTaskRunner: telemetryTaskRunner(server), - }, - }); -} - -export function telemetryTaskRunner(server) { - return ({ taskInstance }) => { - const { state } = taskInstance; - const prevState = state; - - const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; - - let mapsTelemetryTask; - - return { - async run({ taskCanceled = false } = {}) { - try { - mapsTelemetryTask = makeCancelable(getMapsTelemetry(server, callCluster), taskCanceled); - } catch (err) { - server.log(['warning'], `Error loading maps telemetry: ${err}`); - } finally { - return mapsTelemetryTask.promise - .then((mapsTelemetry = {}) => { - return { - state: { - runs: state.runs || 0 + 1, - stats: mapsTelemetry.attributes || prevState.stats || {}, - }, - runAt: getNextMidnight(), - }; - }) - .catch(errMsg => - server.log(['warning'], `Error executing maps telemetry task: ${errMsg}`) - ); - } - }, - async cancel() { - if (mapsTelemetryTask) { - mapsTelemetryTask.cancel(); - } else { - server.log(['warning'], `Can not cancel "mapsTelemetryTask", it has not been defined`); - } - }, - }; - }; -} - -function makeCancelable(promise, isCanceled) { - const logMsg = 'Maps telemetry task has been cancelled'; - const wrappedPromise = new Promise((resolve, reject) => { - promise - .then(val => (isCanceled ? reject(logMsg) : resolve(val))) - .catch(err => (isCanceled ? reject(logMsg) : reject(err.message))); - }); - - return { - promise: wrappedPromise, - cancel() { - isCanceled = true; - }, - }; -} - -function getNextMidnight() { - const nextMidnight = new Date(); - nextMidnight.setHours(0, 0, 0, 0); - nextMidnight.setDate(nextMidnight.getDate() + 1); - return nextMidnight; -} diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js deleted file mode 100644 index ad23ed16342040..00000000000000 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js +++ /dev/null @@ -1,68 +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 { getMockKbnServer, getMockTaskInstance } from '../test_utils'; -import { telemetryTaskRunner } from './telemetry_task'; -import * as mapsTelemetry from './maps_telemetry'; -jest.mock('./maps_telemetry'); - -const expectedAttributes = { - expect: 'values', - toBe: 'populated', -}; - -const generateTelemetry = ({ includeAttributes = true } = {}) => { - mapsTelemetry.getMapsTelemetry = async () => ({ // eslint-disable-line - attributes: includeAttributes ? expectedAttributes : {}, - }); -}; - -describe('telemetryTaskRunner', () => { - let mockTaskInstance; - let mockKbnServer; - let taskRunner; - - beforeEach(() => { - mockTaskInstance = getMockTaskInstance(); - mockKbnServer = getMockKbnServer(); - taskRunner = telemetryTaskRunner(mockKbnServer)({ taskInstance: mockTaskInstance }); - }); - - test('returns empty stats as default', async () => { - generateTelemetry({ includeAttributes: false }); - - const runResult = await taskRunner.run(); - - expect(runResult).toMatchObject({ - state: { - runs: 1, - stats: {}, - }, - }); - }); - - // Return stats when run normally - test('returns stats normally', async () => { - generateTelemetry(); - - const runResult = await taskRunner.run(); - - expect(runResult).toMatchObject({ - state: { - runs: 1, - stats: expectedAttributes, - }, - }); - }); - - test('cancels when cancel flag set to "true", returns undefined', async () => { - generateTelemetry(); - - const runResult = await taskRunner.run({ taskCanceled: true }); - - expect(runResult).toBe(undefined); - }); -}); diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js index 0bf24be0fa0b05..ef7124e2864f8c 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js @@ -79,6 +79,10 @@ export default function({ getService }) { expect(stats.stack_stats.kibana.plugins.apm.services_per_agent).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.infraops.last_24_hours).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins['maps-telemetry'].attributes.timeCaptured).to.be.a( + 'string' + ); + expect(stats.stack_stats.kibana.plugins.reporting.enabled).to.be(true); expect(stats.stack_stats.kibana.plugins.rollups.index_patterns).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.spaces.available).to.be(true); From c9c80845d0299a7a69ad9bcda8a91168e1423196 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 6 Jan 2020 13:29:13 -0700 Subject: [PATCH 010/137] Check license to typescript (#52955) Co-authored-by: Elastic Machine --- x-pack/legacy/plugins/reporting/index.ts | 2 +- .../{check_license.js => check_license.ts} | 36 ++++++++++++------- .../plugins/reporting/server/lib/index.ts | 1 - 3 files changed, 24 insertions(+), 15 deletions(-) rename x-pack/legacy/plugins/reporting/server/lib/{check_license.js => check_license.ts} (69%) diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index c0c9e458132f05..faa27bfb2d6eae 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -94,7 +94,7 @@ export const reporting = (kibana: any) => { const { xpack_main: xpackMainPlugin } = server.plugins; mirrorPluginStatus(xpackMainPlugin, this); const checkLicense = checkLicenseFactory(exportTypesRegistry); - xpackMainPlugin.status.once('green', () => { + (xpackMainPlugin as any).status.once('green', () => { // Register a function that is called whenever the xpack info changes, // to re-compute the license check results for this plugin xpackMainPlugin.info.feature(this.id).registerLicenseCheckResultsGenerator(checkLicense); diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.js b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts similarity index 69% rename from x-pack/legacy/plugins/reporting/server/lib/check_license.js rename to x-pack/legacy/plugins/reporting/server/lib/check_license.ts index 5a784f9913352c..02e1196f1d00db 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/check_license.js +++ b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts @@ -4,19 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import { XPackInfo } from '../../../xpack_main/server/lib/xpack_info'; +import { XPackInfoLicense } from '../../../xpack_main/server/lib/xpack_info_license'; +import { ExportTypesRegistry, ExportTypeDefinition } from '../../types'; + +interface LicenseCheckResult { + showLinks: boolean; + enableLinks: boolean; + message?: string; +} + const messages = { getUnavailable: () => { return 'You cannot use Reporting because license information is not available at this time.'; }, - getExpired: license => { + getExpired: (license: XPackInfoLicense) => { return `You cannot use Reporting because your ${license.getType()} license has expired.`; }, }; -const makeManagementFeature = exportTypes => { +const makeManagementFeature = ( + exportTypes: Array> +) => { return { id: 'management', - checkLicense: license => { + checkLicense: (license: XPackInfoLicense | null) => { if (!license) { return { showLinks: true, @@ -46,10 +58,12 @@ const makeManagementFeature = exportTypes => { }; }; -const makeExportTypeFeature = exportType => { +const makeExportTypeFeature = ( + exportType: ExportTypeDefinition +) => { return { id: exportType.id, - checkLicense: license => { + checkLicense: (license: XPackInfoLicense | null) => { if (!license) { return { showLinks: true, @@ -84,13 +98,9 @@ const makeExportTypeFeature = exportType => { }; }; -export function checkLicenseFactory(exportTypesRegistry) { - return function checkLicense(xpackLicenseInfo) { - const license = - xpackLicenseInfo === null || !xpackLicenseInfo.isAvailable() - ? null - : xpackLicenseInfo.license; - +export function checkLicenseFactory(exportTypesRegistry: ExportTypesRegistry) { + return function checkLicense(xpackInfo: XPackInfo) { + const license = xpackInfo === null || !xpackInfo.isAvailable() ? null : xpackInfo.license; const exportTypes = Array.from(exportTypesRegistry.getAll()); const reportingFeatures = [ ...exportTypes.map(makeExportTypeFeature), @@ -100,6 +110,6 @@ export function checkLicenseFactory(exportTypesRegistry) { return reportingFeatures.reduce((result, feature) => { result[feature.id] = feature.checkLicense(license); return result; - }, {}); + }, {} as Record); }; } diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index 50d1a276b6b5d0..0a2db749cb954a 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -5,7 +5,6 @@ */ export { getExportTypesRegistry } from './export_types_registry'; -// @ts-ignore untyped module export { checkLicenseFactory } from './check_license'; export { LevelLogger } from './level_logger'; export { cryptoFactory } from './crypto'; From 8d37637c07b2b9498b74780f2d20c62991fa85d8 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 6 Jan 2020 12:44:42 -0800 Subject: [PATCH 011/137] [DOCS][Reporting] Updates introduction and troubleshooting sections" (#54036) * [Reporting/Docs] Various Documentation Additions - Add ECONNREFUSED to troubleshooting page: - System Requirements for Reporting - Layout and Sizing * [DOCS][Reporting] Updates index and troubleshooting pages Co-authored-by: Tim Sullivan --- .../reporting/images/shareable-container.png | Bin 0 -> 112809 bytes docs/user/reporting/index.asciidoc | 44 ++++++++++++---- .../reporting-troubleshooting.asciidoc | 49 +++++++++++++++--- 3 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 docs/user/reporting/images/shareable-container.png diff --git a/docs/user/reporting/images/shareable-container.png b/docs/user/reporting/images/shareable-container.png new file mode 100644 index 0000000000000000000000000000000000000000..db5a41dcff471ceded5a5fca5415a80dbc3fd08e GIT binary patch literal 112809 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 z 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⋙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

v= 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 literal 0 HcmV?d00001 diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 06af9e60384455..fde88130a26b44 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -6,11 +6,11 @@ -- -You can generate a report that contains a {kib} dashboard, visualization, -saved search, or Canvas workpad. Depending on the object type, you can export the data as +You can generate a report that contains a {kib} dashboard, visualization, +saved search, or Canvas workpad. Depending on the object type, you can export the data as a PDF, PNG, or CSV document, which you can keep for yourself, or share with others. -Reporting is available from the *Share* menu +Reporting is available from the *Share* menu in *Discover*, *Visualize*, *Dashboard*, and *Canvas*. [role="screenshot"] @@ -40,9 +40,9 @@ for an example. [[manually-generate-reports]] == Generate a report manually -. Open the dashboard, visualization, Canvas workpad, or saved search that you want to include in the report. +. Open the dashboard, visualization, Canvas workpad, or saved search that you want to include in the report. -. In the {kib} toolbar, click *Share*. If you are working in Canvas, +. In the {kib} toolbar, click *Share*. If you are working in Canvas, click the share icon image:user/reporting/images/canvas-share-button.png["Canvas Share button"]. . Select the option appropriate for your object. You can export: @@ -55,14 +55,36 @@ click the share icon image:user/reporting/images/canvas-share-button.png["Canvas + A notification appears when the report is complete. +[float] +[[reporting-layout-sizing]] +== Layout and sizing +The layout and size of the PDF or PNG image depends on the {kib} app +with which the Reporting plugin is integrated. For Canvas, the +worksheet dimensions determine the size for Reporting. In other apps, +the dimensions are taken on the fly by looking at +the size of the visualization elements or panels on the page. + +The size dimensions are part of the reporting job parameters. Therefore, to +make the report output larger or smaller, you can change the size of the browser. +This resizes the shareable container before generating the +report, so the desired dimensions are passed in the job parameters. + +In the following {kib} dashboard, the shareable container is highlighted. +The shareable container is captured when you click the +*Generate* or *Copy POST URL* button. It might take some trial and error +before you're satisfied with the layout and dimensions in the resulting +PNG or PDF image. + +[role="screenshot"] +image::user/reporting/images/shareable-container.png["Shareable Container"] + + + [float] [[optimize-pdf]] == Optimize PDF for print—dashboard only -By default, {kib} creates a PDF -using the existing layout and size of the dashboard. To create a -printer-friendly PDF with multiple A4 portrait pages and two visualizations -per page, turn on *Optimize for printing*. +To create a printer-friendly PDF with multiple A4 portrait pages and two visualizations per page, turn on *Optimize for printing*. [role="screenshot"] image::user/reporting/images/preserve-layout-switch.png["Share"] @@ -72,8 +94,8 @@ image::user/reporting/images/preserve-layout-switch.png["Share"] [[manage-report-history]] == View and manage report history -For a list of your reports, go to *Management > Reporting*. -From this view, you can monitor the generation of a report and +For a list of your reports, go to *Management > Reporting*. +From this view, you can monitor the generation of a report and download reports that you previously generated. [float] diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index ca7fa6abcc9d9c..dc4ffdfebdae9b 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -7,12 +7,20 @@ Having trouble? Here are solutions to common problems you might encounter while using Reporting. +* <> +* <> +* <> +* <> +* <> +* <> +* <> + [float] [[reporting-troubleshooting-system-dependencies]] === System dependencies Reporting launches a "headless" web browser called Chromium on the Kibana server. It is a custom build made by Elastic of an open source project, and it is intended to have minimal dependencies on OS libraries. However, the Kibana server OS might still require additional -dependencies for Chromium. +dependencies to run the Chromium executable. Make sure Kibana server OS has the appropriate packages installed for the distribution. @@ -33,19 +41,30 @@ If you are using Ubuntu/Debian systems, install the following packages: * `fonts-liberation` * `libfontconfig1` +If the system is missing dependencies, then Reporting will fail in a non-deterministic way. {kib} runs a self-test at server startup, and +if it encounters errors, logs them in the Console. Unfortunately, the error message does not include +information about why Chromium failed to run. The most common error message is `Error: connect ECONNREFUSED`, which indicates +that {kib} could not connect to the Chromium process. + +To troubleshoot the problem, start the {kib} server with environment variables that tell Chromium to print verbose logs. See the +<> for more information. + [float] -=== Text is rendered incorrectly in generated reports +[[reporting-troubleshooting-text-incorrect]] +=== Text rendered incorrectly in generated reports If a report label is rendered as an empty rectangle, no system fonts are available. Install at least one font package on the system. If the report is missing certain Chinese, Japanese or Korean characters, ensure that a system font with those characters is installed. [float] +[[reporting-troubleshooting-missing-data]] === Missing data in PDF report of data table visualization There is currently a known limitation with the Data Table visualization that only the first page of data rows, which are the only data visible on the screen, are shown in PDF reports. [float] +[[reporting-troubleshooting-file-permissions]] === File permissions Ensure that the `headless_shell` binary located in your Kibana data directory is owned by the user who is running Kibana, that the user has the execute permission, and if applicable, that the filesystem is mounted with the `exec` option. @@ -63,25 +82,25 @@ Whenever possible, a Reporting error message tries to be as self-explanatory as along with the solution. [float] -==== "Max attempts reached" +==== Max attempts reached There are two primary causes of this error: -. You're creating a PDF of a visualization or dashboard that spans a large amount of data and Kibana is hitting the `xpack.reporting.queue.timeout` +* You're creating a PDF of a visualization or dashboard that spans a large amount of data and Kibana is hitting the `xpack.reporting.queue.timeout` -. Kibana is hosted behind a reverse-proxy, and the <> are not configured correctly +* Kibana is hosted behind a reverse-proxy, and the <> are not configured correctly Create a Markdown visualization and then create a PDF report. If this succeeds, increase the `xpack.reporting.queue.timeout` setting. If the PDF report fails with "Max attempts reached," check your <>. [float] [[reporting-troubleshooting-nss-dependency]] -==== "You must install nss for Reporting to work" +==== You must install nss for Reporting to work Reporting using the Chromium browser relies on the Network Security Service libraries (NSS). Install the appropriate nss package for your distribution. [float] [[reporting-troubleshooting-sandbox-dependency]] -==== "Unable to use Chromium sandbox" +==== Unable to use Chromium sandbox Chromium uses sandboxing techniques that are built on top of operating system primitives. The Linux sandbox depends on user namespaces, which were introduced with the 3.8 Linux kernel. However, many distributions don't have user namespaces enabled by default, or they require the CAP_SYS_ADMIN capability. @@ -90,6 +109,7 @@ Elastic recommends that you research the feasibility of enabling unprivileged us is if you are running Kibana in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters. [float] +[[reporting-troubleshooting-verbose-logs]] === Verbose logs {kib} server logs have a lot of useful information for troubleshooting and understanding how things work. If you're having any issues at all, the full logs from Reporting will be the first place to look. In `kibana.yml`: @@ -101,10 +121,12 @@ logging.verbose: true For more information about logging, see <>. +[float] +[[reporting-troubleshooting-puppeteer-debug-logs]] === Puppeteer debug logs The Chromium browser that {kib} launches on the server is driven by a NodeJS library for Chromium called Puppeteer. The Puppeteer library has its own command-line method to generate its own debug logs, which can sometimes be helpful, particularly to figure out if a problem is -caused by Kibana or Chromium. See more at https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/README.md#debugging-tips +caused by Kibana or Chromium. See more at https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/README.md#debugging-tips[debugging tips]. Using Puppeteer's debug method when launching Kibana would look like: ``` @@ -114,3 +136,14 @@ The internal DevTools protocol traffic will be logged via the `debug` module und The Puppeteer logs are very verbose and could possibly contain sensitive information. Handle the generated output with care. + +[float] +[[reporting-troubleshooting-system-requirements]] +=== System requirements +In Elastic Cloud, the {kib} instances that most configurations provide by default is for 1GB of RAM for the instance. That is enough for +{kib} Reporting when the visualization or dashboard is relatively simple, such as a single pie chart or a dashboard with +a few visualizations. However, certain visualization types incur more load than others. For example, a TSVB panel has a lot of network +requests to render. + +If the {kib} instance doesn't have enough memory to run the report, the report fails with an error such as `Error: Page crashed!` +In this case, try increasing the memory for the {kib} instance to 2GB. From fa8da7c34911475aa9f5d4abe25876b736e2ca71 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 6 Jan 2020 14:34:27 -0700 Subject: [PATCH 012/137] [Reporting/PDF] Refactor screenshot pipeline for multi-url by default (#48588) * Multi-url pass to screenshotsObservable * Restore "first" operator * max attempt = 1 for testing * cleanup debug * restore more concatMap Co-authored-by: Elastic Machine --- .../execute_job/decrypt_job_headers.test.ts | 2 +- .../common/execute_job/decrypt_job_headers.ts | 12 +- .../get_conditional_headers.test.ts | 22 +-- .../execute_job/get_conditional_headers.ts | 6 +- .../execute_job/get_custom_logo.test.ts | 2 +- .../common/execute_job/get_custom_logo.ts | 12 +- .../common/execute_job/get_full_urls.test.ts | 115 +++++------- .../common/execute_job/get_full_urls.ts | 13 +- .../omit_blacklisted_headers.test.ts | 9 +- .../execute_job/omit_blacklisted_headers.ts | 5 +- .../common/lib/screenshots/index.ts | 169 ++++++------------ .../common/lib/screenshots/scan_page.ts | 30 ++++ .../common/lib/screenshots/types.ts | 4 +- .../png/server/execute_job/index.test.js | 2 +- .../png/server/execute_job/index.ts | 20 +-- .../png/server/lib/generate_png.ts | 12 +- .../printable_pdf/server/execute_job/index.ts | 48 +++-- .../printable_pdf/server/lib/generate_pdf.ts | 23 ++- .../browsers/chromium/driver_factory/index.ts | 91 +++++----- .../reporting/server/browsers/index.ts | 1 + .../server/lib/validate/validate_browser.ts | 16 +- x-pack/test/reporting/configs/chromium_api.js | 1 + 22 files changed, 269 insertions(+), 346 deletions(-) create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts index 9ba7cfbcac1d89..1b7ba3c90bab1b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts @@ -43,7 +43,7 @@ describe('headers', () => { }; const encryptedHeaders = await encryptHeaders(headers); - const { decryptedHeaders } = await decryptJobHeaders({ + const decryptedHeaders = await decryptJobHeaders({ job: { title: 'cool-job-bro', type: 'csv', diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts index 486181117bbfbd..436b2c2dab1ad5 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts @@ -17,22 +17,18 @@ export const decryptJobHeaders = async < JobParamsType, JobDocPayloadType extends HasEncryptedHeaders >({ - job, server, + job, logger, }: { - job: JobDocPayloadType; server: ServerFacade; - logger: Logger; -}): Promise<{ job: JobDocPayloadType; - server: ServerFacade; - decryptedHeaders: Record; -}> => { + logger: Logger; +}): Promise> => { const crypto: CryptoFactory = cryptoFactory(server); try { const decryptedHeaders: Record = await crypto.decrypt(job.headers); - return { job, decryptedHeaders, server }; + return decryptedHeaders; } catch (err) { logger.error(err); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts index 66990c1f37df4f..070bdb4314af9c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts @@ -27,7 +27,7 @@ describe('conditions', () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -44,7 +44,7 @@ describe('conditions', () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -65,7 +65,7 @@ describe('conditions', () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -82,7 +82,7 @@ describe('conditions', () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -97,7 +97,7 @@ describe('conditions', () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -120,7 +120,7 @@ describe('conditions', () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -137,7 +137,7 @@ describe('conditions', () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -153,7 +153,7 @@ test('uses basePath from job when creating saved object service', async () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -180,7 +180,7 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, server: mockServer, @@ -203,7 +203,7 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav describe('config formatting', () => { test(`lowercases server.host`, async () => { mockServer = createMockServer({ settings: { 'server.host': 'COOL-HOSTNAME' } }); - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: {}, server: mockServer, @@ -215,7 +215,7 @@ describe('config formatting', () => { mockServer = createMockServer({ settings: { 'xpack.reporting.kibanaServer.hostname': 'GREAT-HOSTNAME' }, }); - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: { title: 'cool-job-bro', type: 'csv', diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts index 14c092ccfb4a68..975060a8052f07 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts @@ -6,13 +6,13 @@ import { ConditionalHeaders, ServerFacade } from '../../../types'; export const getConditionalHeaders = ({ + server, job, filteredHeaders, - server, }: { + server: ServerFacade; job: JobDocPayloadType; filteredHeaders: Record; - server: ServerFacade; }) => { const config = server.config(); const [hostname, port, basePath, protocol] = [ @@ -32,5 +32,5 @@ export const getConditionalHeaders = ({ }, }; - return { job, conditionalHeaders, server }; + return conditionalHeaders; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts index 4fca05337ea0c7..ff2c44026315dc 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts @@ -19,7 +19,7 @@ test(`gets logo from uiSettings`, async () => { baz: 'quix', }; - const { conditionalHeaders } = await getConditionalHeaders({ + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayloadPDF, filteredHeaders: permittedHeaders, server: mockServer, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts index 9b64e896dad18d..0059276f6df718 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts @@ -9,13 +9,13 @@ import { ConditionalHeaders, ServerFacade } from '../../../types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ + server, job, conditionalHeaders, - server, }: { + server: ServerFacade; job: JobDocPayloadPDF; conditionalHeaders: ConditionalHeaders; - server: ServerFacade; }) => { const serverBasePath: string = server.config().get('server.basePath'); @@ -38,12 +38,8 @@ export const getCustomLogo = async ({ }; const savedObjects = server.savedObjects; - const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(fakeRequest); - const uiSettings = server.uiSettingsServiceFactory({ savedObjectsClient }); - - const logo = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO); - - return { job, conditionalHeaders, logo, server }; + const logo: string = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO); + return { conditionalHeaders, logo }; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts index 60735b4abd4463..e25b94e98d020f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts @@ -22,29 +22,26 @@ beforeEach(() => { }); test(`fails if no URL is passed`, async () => { - await expect( + const fn = () => getFullUrls({ job: {}, server: mockServer, - } as FullUrlsOpts) - ).rejects.toMatchInlineSnapshot( - `[Error: No valid URL fields found in Job Params! Expected \`job.relativeUrl\` or \`job.objects[{ relativeUrl }]\`]` + } as FullUrlsOpts); + expect(fn).toThrowErrorMatchingInlineSnapshot( + `"No valid URL fields found in Job Params! Expected \`job.relativeUrl\` or \`job.objects[{ relativeUrl }]\`"` ); }); test(`fails if URLs are file-protocols for PNGs`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'file://etc/passwd/#/something'; - await expect( + const fn = () => getFullUrls({ - job: { - relativeUrl, - forceNow, - }, + job: { relativeUrl, forceNow }, server: mockServer, - } as FullUrlsOpts) - ).rejects.toMatchInlineSnapshot( - `[Error: Found invalid URL(s), all URLs must be relative: ${relativeUrl}]` + } as FullUrlsOpts); + expect(fn).toThrowErrorMatchingInlineSnapshot( + `"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"` ); }); @@ -52,36 +49,26 @@ test(`fails if URLs are absolute for PNGs`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something'; - await expect( + const fn = () => getFullUrls({ - job: { - relativeUrl, - forceNow, - }, + job: { relativeUrl, forceNow }, server: mockServer, - } as FullUrlsOpts) - ).rejects.toMatchInlineSnapshot( - `[Error: Found invalid URL(s), all URLs must be relative: ${relativeUrl}]` + } as FullUrlsOpts); + expect(fn).toThrowErrorMatchingInlineSnapshot( + `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"` ); }); test(`fails if URLs are file-protocols for PDF`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'file://etc/passwd/#/something'; - await expect( + const fn = () => getFullUrls({ - job: { - objects: [ - { - relativeUrl, - }, - ], - forceNow, - }, + job: { objects: [{ relativeUrl }], forceNow }, server: mockServer, - } as FullUrlsOpts) - ).rejects.toMatchInlineSnapshot( - `[Error: Found invalid URL(s), all URLs must be relative: ${relativeUrl}]` + } as FullUrlsOpts); + expect(fn).toThrowErrorMatchingInlineSnapshot( + `"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"` ); }); @@ -89,7 +76,7 @@ test(`fails if URLs are absolute for PDF`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something'; - await expect( + const fn = () => getFullUrls({ job: { objects: [ @@ -100,59 +87,48 @@ test(`fails if URLs are absolute for PDF`, async () => { forceNow, }, server: mockServer, - } as FullUrlsOpts) - ).rejects.toMatchInlineSnapshot( - `[Error: Found invalid URL(s), all URLs must be relative: ${relativeUrl}]` + } as FullUrlsOpts); + expect(fn).toThrowErrorMatchingInlineSnapshot( + `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"` ); }); test(`fails if any URLs are absolute or file's for PDF`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const objects = [ - { - relativeUrl: '/app/kibana#/something_aaa', - }, + { relativeUrl: '/app/kibana#/something_aaa' }, { relativeUrl: 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something', }, - { - relativeUrl: 'file://etc/passwd/#/something', - }, + { relativeUrl: 'file://etc/passwd/#/something' }, ]; - await expect( + + const fn = () => getFullUrls({ - job: { - objects, - forceNow, - }, + job: { objects, forceNow }, server: mockServer, - } as FullUrlsOpts) - ).rejects.toMatchInlineSnapshot( - `[Error: Found invalid URL(s), all URLs must be relative: ${objects[1].relativeUrl} ${objects[2].relativeUrl}]` + } as FullUrlsOpts); + expect(fn).toThrowErrorMatchingInlineSnapshot( + `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something file://etc/passwd/#/something"` ); }); test(`fails if URL does not route to a visualization`, async () => { - await expect( + const fn = () => getFullUrls({ - job: { - relativeUrl: '/app/phoney', - }, + job: { relativeUrl: '/app/phoney' }, server: mockServer, - } as FullUrlsOpts) - ).rejects.toMatchInlineSnapshot( - `[Error: No valid hash in the URL! A hash is expected for the application to route to the intended visualization.]` + } as FullUrlsOpts); + expect(fn).toThrowErrorMatchingInlineSnapshot( + `"No valid hash in the URL! A hash is expected for the application to route to the intended visualization."` ); }); test(`adds forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const { urls } = await getFullUrls({ - job: { - relativeUrl: '/app/kibana#/something', - forceNow, - }, + const urls = await getFullUrls({ + job: { relativeUrl: '/app/kibana#/something', forceNow }, server: mockServer, } as FullUrlsOpts); @@ -164,11 +140,8 @@ test(`adds forceNow to hash's query, if it exists`, async () => { test(`appends forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const { urls } = await getFullUrls({ - job: { - relativeUrl: '/app/kibana#/something?_g=something', - forceNow, - }, + const urls = await getFullUrls({ + job: { relativeUrl: '/app/kibana#/something?_g=something', forceNow }, server: mockServer, } as FullUrlsOpts); @@ -178,10 +151,8 @@ test(`appends forceNow to hash's query, if it exists`, async () => { }); test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { - const { urls } = await getFullUrls({ - job: { - relativeUrl: '/app/kibana#/something', - }, + const urls = await getFullUrls({ + job: { relativeUrl: '/app/kibana#/something' }, server: mockServer, } as FullUrlsOpts); @@ -190,7 +161,7 @@ test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { test(`adds forceNow to each of multiple urls`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const { urls } = await getFullUrls({ + const urls = await getFullUrls({ job: { objects: [ { relativeUrl: '/app/kibana#/something_aaa' }, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts index 59c748aba96627..90049e8e3aea48 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts @@ -12,7 +12,7 @@ import { } from 'url'; import { getAbsoluteUrlFactory } from '../../../common/get_absolute_url'; import { validateUrls } from '../../../common/validate_urls'; -import { ServerFacade, ConditionalHeaders } from '../../../types'; +import { ServerFacade } from '../../../types'; import { JobDocPayloadPNG } from '../../png/types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; @@ -23,15 +23,12 @@ function isPdfJob(job: JobDocPayloadPNG | JobDocPayloadPDF): job is JobDocPayloa return (job as JobDocPayloadPDF).objects !== undefined; } -export async function getFullUrls({ - job, +export function getFullUrls({ server, - ...mergeValues // pass-throughs + job, }: { - job: JobDocPayloadPDF | JobDocPayloadPNG; server: ServerFacade; - conditionalHeaders: ConditionalHeaders; - logo?: string; + job: JobDocPayloadPDF | JobDocPayloadPNG; }) { const config = server.config(); @@ -96,5 +93,5 @@ export async function getFullUrls({ }); }); - return { job, server, urls, ...mergeValues }; + return urls; } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts index 4d4b0c8ade3f69..f446369fec78ce 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts @@ -4,14 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../../../test_helpers/create_mock_server'; import { omitBlacklistedHeaders } from './index'; -let mockServer: any; -beforeEach(() => { - mockServer = createMockServer(''); -}); - test(`omits blacklisted headers`, async () => { const permittedHeaders = { foo: 'bar', @@ -27,7 +21,7 @@ test(`omits blacklisted headers`, async () => { 'transfer-encoding': '', }; - const { filteredHeaders } = await omitBlacklistedHeaders({ + const filteredHeaders = await omitBlacklistedHeaders({ job: { title: 'cool-job-bro', type: 'csv', @@ -41,7 +35,6 @@ test(`omits blacklisted headers`, async () => { ...permittedHeaders, ...blacklistedHeaders, }, - server: mockServer, }); expect(filteredHeaders).toEqual(permittedHeaders); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts index 1bd52a0f1b2d2a..cbebd6bc21b0e6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts @@ -5,20 +5,17 @@ */ import { omit } from 'lodash'; import { KBN_SCREENSHOT_HEADER_BLACKLIST } from '../../../common/constants'; -import { ServerFacade } from '../../../types'; export const omitBlacklistedHeaders = ({ job, decryptedHeaders, - server, }: { job: JobDocPayloadType; decryptedHeaders: Record; - server: ServerFacade; }) => { const filteredHeaders: Record = omit( decryptedHeaders, KBN_SCREENSHOT_HEADER_BLACKLIST ); - return { job, filteredHeaders, server }; + return filteredHeaders; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index 152ef32e331b98..b83021d5e38dd8 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -5,21 +5,9 @@ */ import * as Rx from 'rxjs'; -import { first, mergeMap } from 'rxjs/operators'; -import { - ServerFacade, - CaptureConfig, - HeadlessChromiumDriverFactory, - HeadlessChromiumDriver as HeadlessBrowser, -} from '../../../../types'; -import { - ElementsPositionAndAttribute, - ScreenshotResults, - ScreenshotObservableOpts, - TimeRange, -} from './types'; - -import { checkForToastMessage } from './check_for_toast'; +import { first, mergeMap, toArray } from 'rxjs/operators'; +import { ServerFacade, CaptureConfig, HeadlessChromiumDriverFactory } from '../../../../types'; +import { ScreenshotResults, ScreenshotObservableOpts } from './types'; import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; import { waitForRenderComplete } from './wait_for_render'; @@ -28,6 +16,7 @@ import { waitForElementsToBeInDOM } from './wait_for_dom_elements'; import { getTimeRange } from './get_time_range'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getScreenshots } from './get_screenshots'; +import { scanPage } from './scan_page'; import { skipTelemetry } from './skip_telemetry'; export function screenshotsObservableFactory( @@ -39,108 +28,68 @@ export function screenshotsObservableFactory( return function screenshotsObservable({ logger, - url, + urls, conditionalHeaders, layout, browserTimezone, - }: ScreenshotObservableOpts): Rx.Observable { - const create$ = browserDriverFactory.create({ - viewport: layout.getBrowserViewport(), - browserTimezone, - }); + }: ScreenshotObservableOpts): Rx.Observable { + const create$ = browserDriverFactory.createPage( + { viewport: layout.getBrowserViewport(), browserTimezone }, + logger + ); - // @ts-ignore this needs to be refactored to use less random type declaration and instead rely on structures that work with inference TODO - return create$.pipe( - mergeMap(({ driver$, exit$ }) => { - const screenshot$ = driver$.pipe( - mergeMap( - (browser: HeadlessBrowser) => openUrl(browser, url, conditionalHeaders, logger), - browser => browser - ), - mergeMap( - (browser: HeadlessBrowser) => skipTelemetry(browser, logger), - browser => browser - ), - mergeMap( - (browser: HeadlessBrowser) => { - logger.debug( - 'waiting for elements or items count attribute; or not found to interrupt' - ); + return Rx.from(urls).pipe( + mergeMap(url => { + return create$.pipe( + mergeMap(({ driver, exit$ }) => { + const screenshot$ = Rx.of(driver).pipe( + mergeMap(() => openUrl(driver, url, conditionalHeaders, logger)), + mergeMap(() => skipTelemetry(driver, logger)), + mergeMap(() => scanPage(driver, layout, logger)), + mergeMap(() => getNumberOfItems(driver, layout, logger)), + mergeMap(async itemsCount => { + const viewport = layout.getViewport(itemsCount); + await Promise.all([ + driver.setViewport(viewport, logger), + waitForElementsToBeInDOM(driver, itemsCount, layout, logger), + ]); + }), + mergeMap(async () => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + await injectCustomCss(driver, layout, logger); - // the dashboard is using the `itemsCountAttribute` attribute to let us - // know how many items to expect since gridster incrementally adds panels - // we have to use this hint to wait for all of them - const renderSuccess = browser.waitForSelector( - `${layout.selectors.renderComplete},[${layout.selectors.itemsCountAttribute}]`, - {}, - logger - ); - const renderError = checkForToastMessage(browser, layout, logger); - return Rx.race(Rx.from(renderSuccess), Rx.from(renderError)); - }, - browser => browser - ), - mergeMap( - (browser: HeadlessBrowser) => getNumberOfItems(browser, layout, logger), - (browser, itemsCount: number) => ({ browser, itemsCount }) - ), - mergeMap( - async ({ browser, itemsCount }) => { - logger.debug('setting viewport'); - const viewport = layout.getViewport(itemsCount); - return await browser.setViewport(viewport, logger); - }, - ({ browser, itemsCount }) => ({ browser, itemsCount }) - ), - mergeMap( - ({ browser, itemsCount }) => - waitForElementsToBeInDOM(browser, itemsCount, layout, logger), - ({ browser }) => browser - ), - mergeMap( - browser => { - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - return injectCustomCss(browser, layout, logger); - }, - browser => browser - ), - mergeMap( - async browser => { - if (layout.positionElements) { - // position panel elements for print layout - return await layout.positionElements(browser, logger); - } - }, - browser => browser - ), - mergeMap( - (browser: HeadlessBrowser) => { - return waitForRenderComplete(captureConfig, browser, layout, logger); - }, - browser => browser - ), - mergeMap( - browser => getTimeRange(browser, layout, logger), - (browser, timeRange: TimeRange | null) => ({ browser, timeRange }) - ), - mergeMap( - ({ browser }) => getElementPositionAndAttributes(browser, layout), - ({ browser, timeRange }, elementsPositionAndAttributes: ElementsPositionAndAttribute[]) => { - return { browser, timeRange, elementsPositionAndAttributes }; - } // prettier-ignore - ), - mergeMap( - ({ browser, elementsPositionAndAttributes }) => { - return getScreenshots({ browser, elementsPositionAndAttributes, logger }); - }, - ({ timeRange }, screenshots) => ({ timeRange, screenshots }) - ) - ); + if (layout.positionElements) { + // position panel elements for print layout + await layout.positionElements(driver, logger); + } - return Rx.race(screenshot$, exit$); + await waitForRenderComplete(captureConfig, driver, layout, logger); + }), + mergeMap(() => getTimeRange(driver, layout, logger)), + mergeMap( + async (timeRange): Promise => { + const elementsPositionAndAttributes = await getElementPositionAndAttributes( + driver, + layout + ); + const screenshots = await getScreenshots({ + browser: driver, + elementsPositionAndAttributes, + logger, + }); + + return { timeRange, screenshots }; + } + ) + ); + + return Rx.race(screenshot$, exit$); + }) + ); }), - first() + first(), + toArray() ); }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts new file mode 100644 index 00000000000000..81ff01bb204b86 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/scan_page.ts @@ -0,0 +1,30 @@ +/* + * 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 Rx from 'rxjs'; +import { HeadlessChromiumDriver } from '../../../../server/browsers/chromium/driver'; +import { LevelLogger } from '../../../../server/lib'; +import { LayoutInstance } from '../../layouts/layout'; +import { checkForToastMessage } from './check_for_toast'; + +export function scanPage( + browser: HeadlessChromiumDriver, + layout: LayoutInstance, + logger: LevelLogger +) { + logger.debug('waiting for elements or items count attribute; or not found to interrupt'); + + // the dashboard is using the `itemsCountAttribute` attribute to let us + // know how many items to expect since gridster incrementally adds panels + // we have to use this hint to wait for all of them + const renderSuccess = browser.waitForSelector( + `${layout.selectors.renderComplete},[${layout.selectors.itemsCountAttribute}]`, + {}, + logger + ); + const renderError = checkForToastMessage(browser, layout, logger); + return Rx.race(Rx.from(renderSuccess), Rx.from(renderError)); +} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts index 4f461ef4ec5f98..78cd42f0cae2fd 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -10,7 +10,7 @@ import { LayoutInstance } from '../../layouts/layout'; export interface ScreenshotObservableOpts { logger: LevelLogger; - url: string; + urls: string[]; conditionalHeaders: ConditionalHeaders; layout: LayoutInstance; browserTimezone: string; @@ -36,6 +36,6 @@ export interface Screenshot { } export interface ScreenshotResults { - timeRange: TimeRange; + timeRange: TimeRange | null; screenshots: Screenshot[]; } diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index 0888d287af07c6..1be65722fa668b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -27,7 +27,7 @@ beforeEach(() => { 'server.port': 5601, }; mockServer = { - expose: () => {}, + expose: () => {}, // NOTE: this is for oncePerServer config: memoize(() => ({ get: jest.fn() })), info: { protocol: 'http', diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index b289ae45dde678..c2fda05fbe3e93 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -5,7 +5,7 @@ */ import * as Rx from 'rxjs'; -import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; +import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PLUGIN_ID, PNG_JOB_TYPE } from '../../../../common/constants'; import { ServerFacade, @@ -32,18 +32,14 @@ export const executeJobFactory: QueuedPngExecutorFactory = function executeJobFa const generatePngObservable = generatePngObservableFactory(server, browserDriverFactory); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PNG_JOB_TYPE, 'execute']); - return function executeJob( - jobId: string, - jobToExecute: JobDocPayloadPNG, - cancellationToken: any - ) { + return function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { const jobLogger = logger.clone([jobId]); - const process$ = Rx.of({ job: jobToExecute, server, logger }).pipe( - mergeMap(decryptJobHeaders), - map(omitBlacklistedHeaders), - map(getConditionalHeaders), - mergeMap(getFullUrls), - mergeMap(({ job, conditionalHeaders, urls }) => { + const process$ = Rx.of(1).pipe( + mergeMap(() => decryptJobHeaders({ server, job, logger })), + map(decryptedHeaders => omitBlacklistedHeaders({ job, decryptedHeaders })), + map(filteredHeaders => getConditionalHeaders({ server, job, filteredHeaders })), + mergeMap(conditionalHeaders => { + const urls = getFullUrls({ server, job }); const hashUrl = urls[0]; return generatePngObservable( jobLogger, diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index e2b1474515786e..600762c451a791 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -32,19 +32,17 @@ export function generatePngObservableFactory( const layout = new PreserveLayout(layoutParams.dimensions); const screenshots$ = screenshotsObservable({ logger, - url, + urls: [url], conditionalHeaders, layout, browserTimezone, }).pipe( - map(urlScreenshots => { - if (urlScreenshots.screenshots.length !== 1) { - throw new Error( - `Expected there to be 1 screenshot, but there are ${urlScreenshots.screenshots.length}` - ); + map(([{ screenshots }]) => { + if (screenshots.length !== 1) { + throw new Error(`Expected there to be 1 screenshot, but there are ${screenshots.length}`); } - return urlScreenshots.screenshots[0].base64EncodedData; + return screenshots[0].base64EncodedData; }) ); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index e2b3183464cf24..d85207e6712125 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -5,7 +5,7 @@ */ import * as Rx from 'rxjs'; -import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; +import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { ServerFacade, ExecuteJobFactory, @@ -33,33 +33,28 @@ export const executeJobFactory: QueuedPdfExecutorFactory = function executeJobFa const generatePdfObservable = generatePdfObservableFactory(server, browserDriverFactory); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'execute']); - return function executeJob( - jobId: string, - jobToExecute: JobDocPayloadPDF, - cancellationToken: any - ) { + return function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { const jobLogger = logger.clone([jobId]); - const process$ = Rx.of({ job: jobToExecute, server, logger }).pipe( - mergeMap(decryptJobHeaders), - map(omitBlacklistedHeaders), - map(getConditionalHeaders), - mergeMap(getCustomLogo), - mergeMap(getFullUrls), - mergeMap( - ({ job, conditionalHeaders, logo, urls }): Rx.Observable => { - const { browserTimezone, layout } = jobToExecute; - return generatePdfObservable( - jobLogger, - job.title, - urls, - browserTimezone, - conditionalHeaders, - layout, - logo - ); - } - ), + const process$ = Rx.of(1).pipe( + mergeMap(() => decryptJobHeaders({ server, job, logger })), + map(decryptedHeaders => omitBlacklistedHeaders({ job, decryptedHeaders })), + map(filteredHeaders => getConditionalHeaders({ server, job, filteredHeaders })), + mergeMap(conditionalHeaders => getCustomLogo({ server, job, conditionalHeaders })), + mergeMap(({ logo, conditionalHeaders }) => { + const urls = getFullUrls({ server, job }); + + const { browserTimezone, layout, title } = job; + return generatePdfObservable( + jobLogger, + title, + urls, + browserTimezone, + conditionalHeaders, + layout, + logo + ); + }), map((buffer: Buffer) => ({ content_type: 'application/pdf', content: buffer.toString('base64'), @@ -72,7 +67,6 @@ export const executeJobFactory: QueuedPdfExecutorFactory = function executeJobFa ); const stop$ = Rx.fromEventPattern(cancellationToken.on); - return process$.pipe(takeUntil(stop$)).toPromise(); }; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index 898a13a2dfe805..9a8db308bea794 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -5,7 +5,7 @@ */ import * as Rx from 'rxjs'; -import { toArray, mergeMap } from 'rxjs/operators'; +import { mergeMap } from 'rxjs/operators'; import { groupBy } from 'lodash'; import { LevelLogger } from '../../../../server/lib'; import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types'; @@ -31,7 +31,6 @@ export function generatePdfObservableFactory( browserDriverFactory: HeadlessChromiumDriverFactory ) { const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory); - const captureConcurrency = 1; return function generatePdfObservable( logger: LevelLogger, @@ -41,15 +40,16 @@ export function generatePdfObservableFactory( conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams, logo?: string - ) { + ): Rx.Observable { const layout = createLayout(server, layoutParams) as LayoutInstance; - const screenshots$ = Rx.from(urls).pipe( - mergeMap( - url => screenshotsObservable({ logger, url, conditionalHeaders, layout, browserTimezone }), - captureConcurrency - ), - toArray(), - mergeMap(async (urlScreenshots: ScreenshotResults[]) => { + const screenshots$ = screenshotsObservable({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, + }).pipe( + mergeMap(async urlScreenshots => { const pdfOutput = pdf.create(layout, logo); if (title) { @@ -68,8 +68,7 @@ export function generatePdfObservableFactory( }); pdfOutput.generate(); - const buffer = await pdfOutput.getBuffer(); - return buffer; + return await pdfOutput.getBuffer(); }) ); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index daa7df343f8aa7..6fa46b893de8ce 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -31,10 +31,11 @@ type queueTimeout = number; export class HeadlessChromiumDriverFactory { private binaryPath: binaryPath; - private logger: Logger; private browserConfig: BrowserConfig; private queueTimeout: queueTimeout; private networkPolicy: NetworkPolicy; + private userDataDir: string; + private getChromiumArgs: (viewport: BrowserConfig['viewport']) => string[]; constructor( binaryPath: binaryPath, @@ -46,23 +47,30 @@ export class HeadlessChromiumDriverFactory { this.binaryPath = binaryPath; this.browserConfig = browserConfig; this.queueTimeout = queueTimeout; - this.logger = logger; this.networkPolicy = networkPolicy; + + this.userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chromium-')); + this.getChromiumArgs = (viewport: BrowserConfig['viewport']) => + args({ + userDataDir: this.userDataDir, + viewport, + disableSandbox: this.browserConfig.disableSandbox, + proxy: this.browserConfig.proxy, + }); } type = 'chromium'; - test({ viewport }: { viewport: BrowserConfig['viewport'] }, logger: Logger) { - const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chromium-')); + test(logger: Logger) { const chromiumArgs = args({ - userDataDir, - viewport, + userDataDir: this.userDataDir, + viewport: { width: 800, height: 600 }, disableSandbox: this.browserConfig.disableSandbox, proxy: this.browserConfig.proxy, }); return puppeteerLaunch({ - userDataDir, + userDataDir: this.userDataDir, executablePath: this.binaryPath, ignoreHTTPSErrors: true, args: chromiumArgs, @@ -76,33 +84,25 @@ export class HeadlessChromiumDriverFactory { }); } - create({ - viewport, - browserTimezone, - }: { - viewport: BrowserConfig['viewport']; - browserTimezone: string; - }): Rx.Observable<{ - driver$: Rx.Observable; - exit$: Rx.Observable; - }> { + /* + * Return an observable to objects which will drive screenshot capture for a page + */ + createPage( + { viewport, browserTimezone }: { viewport: BrowserConfig['viewport']; browserTimezone: string }, + pLogger: Logger + ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { return Rx.Observable.create(async (observer: InnerSubscriber) => { - this.logger.debug(`Creating browser driver factory`); + const logger = pLogger.clone(['browser-driver']); + logger.info(`Creating browser page driver`); - const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chromium-')); - const chromiumArgs = args({ - userDataDir, - viewport, - disableSandbox: this.browserConfig.disableSandbox, - proxy: this.browserConfig.proxy, - }); + const chromiumArgs = this.getChromiumArgs(viewport); let browser: Browser; let page: Page; try { browser = await puppeteerLaunch({ pipe: !this.browserConfig.inspect, - userDataDir, + userDataDir: this.userDataDir, executablePath: this.binaryPath, ignoreHTTPSErrors: true, args: chromiumArgs, @@ -119,7 +119,7 @@ export class HeadlessChromiumDriverFactory { // "TimeoutError: waiting for selector ".application" failed: timeout 30000ms exceeded" page.setDefaultTimeout(this.queueTimeout); - this.logger.debug(`Browser driver factory created`); + logger.debug(`Browser page driver created`); } catch (err) { observer.error(new Error(`Error spawning Chromium browser: [${err}]`)); throw err; @@ -130,12 +130,12 @@ export class HeadlessChromiumDriverFactory { await browser.close(); }, }; - const { terminate$ } = safeChildProcess(this.logger, childProcess); + const { terminate$ } = safeChildProcess(logger, childProcess); // this is adding unsubscribe logic to our observer // so that if our observer unsubscribes, we terminate our child-process observer.add(() => { - this.logger.debug(`The browser process observer has unsubscribed. Closing the browser...`); + logger.debug(`The browser process observer has unsubscribed. Closing the browser...`); childProcess.kill(); // ignore async }); @@ -144,7 +144,7 @@ export class HeadlessChromiumDriverFactory { terminate$ .pipe( tap(signal => { - this.logger.debug(`Termination signal received: ${signal}`); + logger.debug(`Termination signal received: ${signal}`); }), ignoreElements() ) @@ -152,33 +152,40 @@ export class HeadlessChromiumDriverFactory { ); // taps the browser log streams and combine them to Kibana logs - this.getBrowserLogger(page).subscribe(); - this.getProcessLogger(browser).subscribe(); + this.getBrowserLogger(page, logger).subscribe(); + this.getProcessLogger(browser, logger).subscribe(); + + // HeadlessChromiumDriver: object to "drive" a browser page + const driver = new HeadlessChromiumDriver(page, { + inspect: this.browserConfig.inspect, + networkPolicy: this.networkPolicy, + }); - const driver$ = Rx.of(new HeadlessChromiumDriver(page, { inspect: this.browserConfig.inspect, networkPolicy: this.networkPolicy })); // prettier-ignore + // Rx.Observable: stream to interrupt page capture const exit$ = this.getPageExit(browser, page); - observer.next({ driver$, exit$ }); + observer.next({ driver, exit$ }); // unsubscribe logic makes a best-effort attempt to delete the user data directory used by chromium observer.add(() => { - this.logger.debug(`deleting chromium user data directory at [${userDataDir}]`); + const userDataDir = this.userDataDir; + logger.debug(`deleting chromium user data directory at [${userDataDir}]`); // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. del(userDataDir).catch(error => { - this.logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); + logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); }); }); }); } - getBrowserLogger(page: Page): Rx.Observable { + getBrowserLogger(page: Page, logger: Logger): Rx.Observable { const consoleMessages$ = Rx.fromEvent(page, 'console').pipe( map(line => { if (line.type() === 'error') { - this.logger.error(line.text(), ['headless-browser-console']); + logger.error(line.text(), ['headless-browser-console']); } else { - this.logger.debug(line.text(), [`headless-browser-console:${line.type()}`]); + logger.debug(line.text(), [`headless-browser-console:${line.type()}`]); } }) ); @@ -187,7 +194,7 @@ export class HeadlessChromiumDriverFactory { map(req => { const failure = req.failure && req.failure(); if (failure) { - this.logger.warning( + logger.warning( `Request to [${req.url()}] failed! [${failure.errorText}]. This error will be ignored.` ); } @@ -197,7 +204,7 @@ export class HeadlessChromiumDriverFactory { return Rx.merge(consoleMessages$, pageRequestFailed$); } - getProcessLogger(browser: Browser) { + getProcessLogger(browser: Browser, logger: Logger): Rx.Observable { const childProcess = browser.process(); // NOTE: The browser driver can not observe stdout and stderr of the child process // Puppeteer doesn't give a handle to the original ChildProcess object @@ -206,7 +213,7 @@ export class HeadlessChromiumDriverFactory { // just log closing of the process const processClose$ = Rx.fromEvent(childProcess, 'close').pipe( tap(() => { - this.logger.debug('child process closed', ['headless-browser-process']); + logger.debug('child process closed', ['headless-browser-process']); }) ); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/index.ts index a5ecc405bf9c59..402fabea56c842 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/index.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 * as chromiumDefinition from './chromium'; export { ensureAllBrowsersDownloaded } from './download'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts index 031709c85284cb..89c49123e85bf2 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts @@ -18,14 +18,12 @@ export const validateBrowser = async ( logger: Logger ) => { if (browserFactory.type === BROWSER_TYPE) { - return browserFactory - .test({ viewport: { width: 800, height: 600 } }, logger) - .then((browser: Browser | null) => { - if (browser && browser.close) { - browser.close(); - } else { - throw new Error('Could not close browser client handle!'); - } - }); + return browserFactory.test(logger).then((browser: Browser | null) => { + if (browser && browser.close) { + browser.close(); + } else { + throw new Error('Could not close browser client handle!'); + } + }); } }; diff --git a/x-pack/test/reporting/configs/chromium_api.js b/x-pack/test/reporting/configs/chromium_api.js index f016738c0e052d..95649dfb5d7a3c 100644 --- a/x-pack/test/reporting/configs/chromium_api.js +++ b/x-pack/test/reporting/configs/chromium_api.js @@ -28,6 +28,7 @@ export default async function({ readConfigFile }) { '["info","warning","error","fatal","optimize","reporting"]', '--xpack.endpoint.enabled=true', '--xpack.reporting.csv.enablePanelActionDownload=true', + '--xpack.reporting.capture.maxAttempts=1', '--xpack.security.session.idleTimeout=3600000', '--xpack.spaces.enabled=false', ], From ecab2073ae8a730d9913d4b66c0e3f5181b1d92c Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Mon, 6 Jan 2020 23:58:55 +0200 Subject: [PATCH 013/137] =?UTF-8?q?[Telemetry]=20fix=20bug=20where=20uiSta?= =?UTF-8?q?tsMetrics=20is=20not=20getting=20report=E2=80=A6=20(#54045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix bug where uiStatsMetrics is not getting reported * fix tests --- packages/kbn-analytics/src/report.ts | 24 ++++++++--------- .../apis/ui_metric/ui_metric.js | 26 +++++++------------ 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/kbn-analytics/src/report.ts b/packages/kbn-analytics/src/report.ts index 1c0b37966355fb..16c0a3069e5fdb 100644 --- a/packages/kbn-analytics/src/report.ts +++ b/packages/kbn-analytics/src/report.ts @@ -78,6 +78,7 @@ export class ReportManager { } assignReports(newMetrics: Metric | Metric[]) { wrapArray(newMetrics).forEach(newMetric => this.assignReport(this.report, newMetric)); + return { report: this.report }; } static createMetricKey(metric: Metric): string { switch (metric.type) { @@ -101,7 +102,7 @@ export class ReportManager { case METRIC_TYPE.USER_AGENT: { const { appName, type, userAgent } = metric; if (userAgent) { - this.report.userAgent = { + report.userAgent = { [key]: { key, appName, @@ -110,23 +111,22 @@ export class ReportManager { }, }; } + return; } case METRIC_TYPE.CLICK: case METRIC_TYPE.LOADED: case METRIC_TYPE.COUNT: { const { appName, type, eventName, count } = metric; - if (report.uiStatsMetrics) { - const existingStats = (report.uiStatsMetrics[key] || {}).stats; - this.report.uiStatsMetrics = this.report.uiStatsMetrics || {}; - this.report.uiStatsMetrics[key] = { - key, - appName, - eventName, - type, - stats: this.incrementStats(count, existingStats), - }; - } + report.uiStatsMetrics = report.uiStatsMetrics || {}; + const existingStats = (report.uiStatsMetrics[key] || {}).stats; + report.uiStatsMetrics[key] = { + key, + appName, + eventName, + type, + stats: this.incrementStats(count, existingStats), + }; return; } default: diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index 5b02ba7e72430f..5ddbd8649589c7 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.js +++ b/test/api_integration/apis/ui_metric/ui_metric.js @@ -25,15 +25,13 @@ export default function({ getService }) { const es = getService('legacyEs'); const createStatsMetric = eventName => ({ - key: ReportManager.createMetricKey({ appName: 'myApp', type: METRIC_TYPE.CLICK, eventName }), eventName, appName: 'myApp', type: METRIC_TYPE.CLICK, - stats: { sum: 1, avg: 1, min: 1, max: 1 }, + count: 1, }); const createUserAgentMetric = appName => ({ - key: ReportManager.createMetricKey({ appName, type: METRIC_TYPE.USER_AGENT }), appName, type: METRIC_TYPE.USER_AGENT, userAgent: @@ -42,12 +40,9 @@ export default function({ getService }) { describe('ui_metric API', () => { it('increments the count field in the document defined by the {app}/{action_type} path', async () => { + const reportManager = new ReportManager(); const uiStatsMetric = createStatsMetric('myEvent'); - const report = { - uiStatsMetrics: { - [uiStatsMetric.key]: uiStatsMetric, - }, - }; + const { report } = reportManager.assignReports([uiStatsMetric]); await supertest .post('/api/ui_metric/report') .set('kbn-xsrf', 'kibana') @@ -61,21 +56,18 @@ export default function({ getService }) { }); it('supports multiple events', async () => { + const reportManager = new ReportManager(); const userAgentMetric = createUserAgentMetric('kibana'); const uiStatsMetric1 = createStatsMetric('myEvent'); const hrTime = process.hrtime(); const nano = hrTime[0] * 1000000000 + hrTime[1]; const uniqueEventName = `myEvent${nano}`; const uiStatsMetric2 = createStatsMetric(uniqueEventName); - const report = { - userAgent: { - [userAgentMetric.key]: userAgentMetric, - }, - uiStatsMetrics: { - [uiStatsMetric1.key]: uiStatsMetric1, - [uiStatsMetric2.key]: uiStatsMetric2, - }, - }; + const { report } = reportManager.assignReports([ + userAgentMetric, + uiStatsMetric1, + uiStatsMetric2, + ]); await supertest .post('/api/ui_metric/report') .set('kbn-xsrf', 'kibana') From 9481cbf36e359d6e60ffb3c1b00c1ad8ac6e8d17 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 6 Jan 2020 18:00:49 -0500 Subject: [PATCH 014/137] [Uptime] Prefer Kibana core http services (#53726) * Prefer Kibana core http service to other fetch functions, refactor helper functions to hooks. * Reintroduce newline deleted in previous commit. * Clean up obsolete import. * Clean up effect code in new hook. * Clean up enum usage in new hook. * Implement PR feedback on new hook. * Fix eslint error. Co-authored-by: Elastic Machine --- .../plugins/uptime/public/apps/plugin.ts | 8 +- .../components/functional/kuery_bar/index.tsx | 15 +- .../plugins/uptime/public/hooks/index.ts | 2 + .../uptime/public/hooks/use_index_pattern.ts | 20 +++ .../uptime/public/hooks/use_telemetry.ts | 41 +++++ .../framework/new_platform_adapter.tsx | 14 +- .../__tests__/get_index_pattern.test.ts | 50 ------- .../index_pattern/get_index_pattern.ts | 26 ---- .../lib/adapters/index_pattern/index.ts | 7 - .../public/lib/adapters/telemetry/index.ts | 8 - .../lib/adapters/telemetry/log_monitor.ts | 18 --- .../lib/adapters/telemetry/log_overview.ts | 18 --- .../plugins/uptime/public/pages/monitor.tsx | 14 +- .../plugins/uptime/public/pages/overview.tsx | 17 +-- .../plugins/uptime/public/uptime_app.tsx | 141 +++++++++--------- 15 files changed, 156 insertions(+), 243 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/public/hooks/use_index_pattern.ts create mode 100644 x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/__tests__/get_index_pattern.test.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/get_index_pattern.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/index.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/index.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_monitor.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_overview.ts diff --git a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts index bc4e30b79cb157..c09fdf116e790a 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts @@ -30,14 +30,8 @@ export class Plugin { } public start(start: StartObject): void { - const { - core, - plugins: { - data: { autocomplete }, - }, - } = start; const libs: UMFrontendLibs = { - framework: getKibanaFrameworkAdapter(core, autocomplete), + framework: getKibanaFrameworkAdapter(start.core, start.plugins), }; // @ts-ignore improper type description this.chrome.setRootTemplate(template); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index 72e88d28240737..731f560d315d63 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useContext } from 'react'; +import React, { useState, useEffect } from 'react'; import { uniqueId, startsWith } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { Typeahead } from './typeahead'; -import { getIndexPattern } from '../../../lib/adapters/index_pattern'; -import { UptimeSettingsContext } from '../../../contexts'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; import { @@ -20,6 +18,7 @@ import { esKuery, IIndexPattern, } from '../../../../../../../../src/plugins/data/public'; +import { useIndexPattern } from '../../../hooks'; const Container = styled.div` margin-bottom: 10px; @@ -71,16 +70,18 @@ export function KueryBar({ autocomplete }: Props) { suggestions: [], isLoadingIndexPattern: true, }); - const { basePath } = useContext(UptimeSettingsContext); const [indexPattern, setIndexPattern] = useState(undefined); const [isLoadingIndexPattern, setIsLoadingIndexPattern] = useState(true); const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); let currentRequestCheck: string; + useIndexPattern((result: any) => setIndexPattern(toStaticIndexPattern(result))); + useEffect(() => { - getIndexPattern(basePath, (result: any) => setIndexPattern(toStaticIndexPattern(result))); - setIsLoadingIndexPattern(false); - }, [basePath]); + if (indexPattern !== undefined) { + setIsLoadingIndexPattern(false); + } + }, [indexPattern]); const [getUrlParams, updateUrlParams] = useUrlParams(); const { search: kuery } = getUrlParams(); diff --git a/x-pack/legacy/plugins/uptime/public/hooks/index.ts b/x-pack/legacy/plugins/uptime/public/hooks/index.ts index 22de59833b08d3..aa7bb0a220357d 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/index.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/index.ts @@ -5,3 +5,5 @@ */ export { useUrlParams } from './use_url_params'; +export { useIndexPattern } from './use_index_pattern'; +export * from './use_telemetry'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_index_pattern.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_index_pattern.ts new file mode 100644 index 00000000000000..eb9b475a35716e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_index_pattern.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, Dispatch } from 'react'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +export const useIndexPattern = (setIndexPattern: Dispatch) => { + const core = useKibana(); + useEffect(() => { + const fetch = core.services.http?.fetch; + async function getIndexPattern() { + if (!fetch) throw new Error('Http core services are not defined'); + setIndexPattern(await fetch('/api/uptime/index_pattern', { method: 'GET' })); + } + getIndexPattern(); + }, [core.services.http, setIndexPattern]); +}; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts new file mode 100644 index 00000000000000..15f276174e2cf1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; +import { HttpHandler } from 'kibana/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +export enum UptimePage { + Overview = '/api/uptime/logOverview', + Monitor = '/api/uptime/logMonitor', + NotFound = '__not-found__', +} + +const getApiPath = (page?: UptimePage) => { + if (!page) throw new Error('Telemetry logging for this page not yet implemented'); + if (page === '__not-found__') + throw new Error('Telemetry logging for 404 page not yet implemented'); + return page.valueOf(); +}; + +const logPageLoad = async (fetch: HttpHandler, page?: UptimePage) => { + try { + await fetch(getApiPath(page), { + method: 'POST', + }); + } catch (e) { + throw e; + } +}; + +export const useUptimeTelemetry = (page?: UptimePage) => { + const kibana = useKibana(); + const fetch = kibana.services.http?.fetch; + useEffect(() => { + if (!fetch) throw new Error('Core http services are not defined'); + logPageLoad(fetch, page); + }, [fetch, page]); +}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index b7ff3b2aa6264e..28179c229013b5 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ChromeBreadcrumb, CoreStart } from 'src/core/public'; +import { ChromeBreadcrumb, LegacyCoreStart } from 'src/core/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { get } from 'lodash'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; import { i18n as i18nFormatter } from '@kbn/i18n'; +import { PluginsStart } from 'ui/new_platform/new_platform'; import { CreateGraphQLClient } from './framework_adapter_types'; import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; import { getIntegratedAppAvailability } from './capabilities_adapter'; @@ -19,13 +19,12 @@ import { DEFAULT_DARK_MODE, DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../../../common/constants'; -import { getTelemetryMonitorPageLogger, getTelemetryOverviewPageLogger } from '../telemetry'; import { UMFrameworkAdapter, BootstrapUptimeApp } from '../../lib'; import { createApolloClient } from './apollo_client_adapter'; export const getKibanaFrameworkAdapter = ( - core: CoreStart, - autocomplete: Pick + core: LegacyCoreStart, + plugins: PluginsStart ): UMFrameworkAdapter => { const { application: { capabilities }, @@ -44,10 +43,10 @@ export const getKibanaFrameworkAdapter = ( ); const canSave = get(capabilities, 'uptime.save', false); const props: UptimeAppProps = { - autocomplete, basePath: basePath.get(), canSave, client: createApolloClient(`${basePath.get()}/api/uptime/graphql`, 'true'), + core, darkMode: core.uiSettings.get(DEFAULT_DARK_MODE), commonlyUsedRanges: core.uiSettings.get(DEFAULT_TIMEPICKER_QUICK_RANGES), i18n, @@ -55,8 +54,7 @@ export const getKibanaFrameworkAdapter = ( isInfraAvailable: infrastructure, isLogsAvailable: logs, kibanaBreadcrumbs: breadcrumbs, - logMonitorPageLoad: getTelemetryMonitorPageLogger('true', basePath.get()), - logOverviewPageLoad: getTelemetryOverviewPageLogger('true', basePath.get()), + plugins, renderGlobalHelpControls: () => setHelpExtension({ appName: i18nFormatter.translate('xpack.uptime.header.appName', { diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/__tests__/get_index_pattern.test.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/__tests__/get_index_pattern.test.ts deleted file mode 100644 index 6654def2f944bc..00000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/__tests__/get_index_pattern.test.ts +++ /dev/null @@ -1,50 +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 axios, { AxiosRequestConfig } from 'axios'; -import { getIndexPattern } from '../get_index_pattern'; - -describe('getIndexPattern', () => { - let axiosSpy: jest.SpyInstance, [string, (AxiosRequestConfig | undefined)?]>; - beforeEach(() => { - axiosSpy = jest.spyOn(axios, 'get'); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('returns expected data', async () => { - expect.assertions(3); - axiosSpy.mockReturnValue(new Promise(r => r({ data: { foo: 'bar' } }))); - expect(await getIndexPattern()).toEqual({ foo: 'bar' }); - expect(axiosSpy.mock.calls).toHaveLength(1); - expect(axiosSpy.mock.calls[0]).toEqual(['/api/uptime/index_pattern']); - }); - - it('handles the supplied basePath', async () => { - expect.assertions(2); - await getIndexPattern('foo'); - expect(axiosSpy.mock.calls).toHaveLength(1); - expect(axiosSpy.mock.calls[0]).toEqual(['foo/api/uptime/index_pattern']); - }); - - it('supplies the returned data to the given setter function', async () => { - const mockSetter = jest.fn(); - axiosSpy.mockReturnValue(new Promise(r => r({ data: { foo: 'bar' } }))); - await getIndexPattern(undefined, mockSetter); - expect(mockSetter).toHaveBeenCalled(); - expect(mockSetter).toHaveBeenCalledWith({ foo: 'bar' }); - }); - - it('returns undefined when there is an error fetching', async () => { - expect.assertions(1); - axiosSpy.mockReturnValue( - new Promise((resolve, reject) => reject('Request timeout, server could not be reached')) - ); - expect(await getIndexPattern()).toBeUndefined(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/get_index_pattern.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/get_index_pattern.ts deleted file mode 100644 index fd4161b35f7ddb..00000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/get_index_pattern.ts +++ /dev/null @@ -1,26 +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 axios from 'axios'; -import { getApiPath } from '../../helper'; - -/** - * Fetches and returns the uptime index pattern, optionally provides it to - * a given setter function. - * @param basePath - the base path, if any - * @param setter - a callback for use with non-async functions like `useEffect` - */ -export const getIndexPattern = async (basePath?: string, setter?: (data: unknown) => void) => { - try { - const { data } = await axios.get(getApiPath('/api/uptime/index_pattern', basePath)); - if (setter) { - setter(data); - } - return data; - } catch { - return undefined; - } -}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/index.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/index.ts deleted file mode 100644 index 1c84a7bc3b7274..00000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/index_pattern/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 { getIndexPattern } from './get_index_pattern'; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/index.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/index.ts deleted file mode 100644 index 08d8d9a5d40694..00000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/index.ts +++ /dev/null @@ -1,8 +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 { getTelemetryMonitorPageLogger } from './log_monitor'; -export { getTelemetryOverviewPageLogger } from './log_overview'; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_monitor.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_monitor.ts deleted file mode 100644 index 20328497d69a86..00000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_monitor.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios from 'axios'; -import { getApiPath } from '../../helper'; - -/** - * Generates a function to log a page load of the monitor page for Kibana telemetry. - * @returns a function that can log page loads - */ -export const getTelemetryMonitorPageLogger = (xsrf: string, basePath?: string) => async () => { - await axios.post(getApiPath('/api/uptime/logMonitor', basePath), undefined, { - headers: { 'kbn-xsrf': xsrf }, - }); -}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_overview.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_overview.ts deleted file mode 100644 index fd9fd773a18b9f..00000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/telemetry/log_overview.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios from 'axios'; -import { getApiPath } from '../../helper'; - -/** - * Generates a function to log a page load of the overview page for Kibana telemtry. - * @returns a function that can log page loads - */ -export const getTelemetryOverviewPageLogger = (xsrf: string, basePath?: string) => async () => { - await axios.post(getApiPath('/api/uptime/logOverview', basePath), undefined, { - headers: { 'kbn-xsrf': xsrf }, - }); -}; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 8c5649f680fcb5..c8334b2376bc17 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -12,14 +12,13 @@ import { getMonitorPageBreadcrumb } from '../breadcrumbs'; import { MonitorCharts, MonitorPageTitle, PingList } from '../components/functional'; import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeSettingsContext } from '../contexts'; -import { useUrlParams } from '../hooks'; +import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { getTitle } from '../lib/helper/get_title'; import { MonitorStatusDetails } from '../components/functional/monitor_status_details'; interface MonitorPageProps { - logMonitorPageLoad: () => void; match: { params: { monitorId: string } }; // this is the query function provided by Apollo's Client API query: ( @@ -28,12 +27,7 @@ interface MonitorPageProps { setBreadcrumbs: UMUpdateBreadcrumbs; } -export const MonitorPage = ({ - logMonitorPageLoad, - query, - setBreadcrumbs, - match, -}: MonitorPageProps) => { +export const MonitorPage = ({ query, setBreadcrumbs, match }: MonitorPageProps) => { // decode 64 base string, it was decoded to make it a valid url, since monitor id can be a url const monitorId = atob(match.params.monitorId); const [pingListPageCount, setPingListPageCount] = useState(10); @@ -74,9 +68,7 @@ export const MonitorPage = ({ monitorId, }; - useEffect(() => { - logMonitorPageLoad(); - }, [logMonitorPageLoad]); + useUptimeTelemetry(UptimePage.Monitor); useTrackPageview({ app: 'uptime', path: 'monitor' }); useTrackPageview({ app: 'uptime', path: 'monitor', delay: 15000 }); diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 8e72f964ed1289..34bcfb994cd484 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -19,10 +19,9 @@ import { } from '../components/functional'; import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeSettingsContext } from '../contexts'; -import { useUrlParams } from '../hooks'; +import { useIndexPattern, useUrlParams, useUptimeTelemetry, UptimePage } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; -import { getIndexPattern } from '../lib/adapters/index_pattern'; import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper'; import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public'; @@ -34,7 +33,6 @@ interface OverviewPageProps { pathname: string; search: string; }; - logOverviewPageLoad: () => void; setBreadcrumbs: UMUpdateBreadcrumbs; } @@ -54,12 +52,7 @@ const EuiFlexItemStyled = styled(EuiFlexItem)` } `; -export const OverviewPage = ({ - basePath, - autocomplete, - logOverviewPageLoad, - setBreadcrumbs, -}: Props) => { +export const OverviewPage = ({ basePath, autocomplete, setBreadcrumbs }: Props) => { const { colors, setHeadingText } = useContext(UptimeSettingsContext); const [getUrlParams, updateUrl] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); @@ -72,11 +65,11 @@ export const OverviewPage = ({ filters: urlFilters, } = params; const [indexPattern, setIndexPattern] = useState(undefined); + useUptimeTelemetry(UptimePage.Overview); + useIndexPattern(setIndexPattern); useEffect(() => { - getIndexPattern(basePath, setIndexPattern); setBreadcrumbs(getOverviewPageBreadcrumbs()); - logOverviewPageLoad(); if (setHeadingText) { setHeadingText( i18n.translate('xpack.uptime.overviewPage.headerText', { @@ -85,7 +78,7 @@ export const OverviewPage = ({ }) ); } - }, [basePath, logOverviewPageLoad, setBreadcrumbs, setHeadingText]); + }, [basePath, setBreadcrumbs, setHeadingText]); useTrackPageview({ app: 'uptime', path: 'overview' }); useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 }); diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index f72055c52255d8..cecbfe375f5fed 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -13,8 +13,9 @@ import React, { useEffect, useState } from 'react'; import { ApolloProvider } from 'react-apollo'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { I18nStart, ChromeBreadcrumb } from 'src/core/public'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; +import { I18nStart, ChromeBreadcrumb, LegacyCoreStart } from 'src/core/public'; +import { PluginsStart } from 'ui/new_platform/new_platform'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { UMGraphQLClient, UMUpdateBreadcrumbs, UMUpdateBadge } from './lib/lib'; import { MonitorPage, OverviewPage, NotFoundPage } from './pages'; import { UptimeRefreshContext, UptimeSettingsContext, UMSettingsContextValues } from './contexts'; @@ -37,15 +38,14 @@ export interface UptimeAppProps { basePath: string; canSave: boolean; client: UMGraphQLClient; + core: LegacyCoreStart; darkMode: boolean; - autocomplete: Pick; i18n: I18nStart; isApmAvailable: boolean; isInfraAvailable: boolean; isLogsAvailable: boolean; kibanaBreadcrumbs: ChromeBreadcrumb[]; - logMonitorPageLoad: () => void; - logOverviewPageLoad: () => void; + plugins: PluginsStart; routerBasename: string; setBreadcrumbs: UMUpdateBreadcrumbs; setBadge: UMUpdateBadge; @@ -55,18 +55,17 @@ export interface UptimeAppProps { const Application = (props: UptimeAppProps) => { const { - autocomplete, basePath, canSave, client, + core, darkMode, commonlyUsedRanges, i18n: i18nCore, isApmAvailable, isInfraAvailable, isLogsAvailable, - logMonitorPageLoad, - logOverviewPageLoad, + plugins, renderGlobalHelpControls, routerBasename, setBreadcrumbs, @@ -156,70 +155,70 @@ const Application = (props: UptimeAppProps) => { return ( - - { - return ( - - - - -

- - - -

{headingText}

-
-
- - - -
- - - ( - - )} - /> - ( - + + { + return ( + + + + +
+ + + +

{headingText}

+
+
+ + - )} - /> - - -
-
-
-
-
- ); - }} - /> -
+ + + + + ( + + )} + /> + ( + + )} + /> + + +
+ + + + + ); + }} + /> + + ); From fef8485f367785b78426430a08b4b0ddd1eb1ae8 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 6 Jan 2020 16:20:45 -0700 Subject: [PATCH 015/137] [Reporting/Legacy] Remove reporting legacy job params compatibility shim (#52539) * [Reporting/Tests] consolidate functional test configs * remove console.log * trash * Update * add more to the comment * restore historic archive for wip ui functional tests * [Reporting/Legacy] Remove reporting legacy job params compatibility shimmy shim shim * objectType: objectType * fix jest test and get_urls logging * simplify change Co-authored-by: Elastic Machine --- .../common/execute_job/get_full_urls.test.ts | 36 +-- .../common/execute_job/get_full_urls.ts | 6 +- .../server/create_job/compatibility_shim.js | 126 -------- .../create_job/compatibility_shim.test.js | 297 ------------------ .../printable_pdf/server/create_job/index.ts | 32 +- .../server/execute_job/index.test.js | 9 +- .../export_types/printable_pdf/types.d.ts | 6 +- .../public/components/report_listing.tsx | 2 +- .../lib/esqueue/helpers/create_index.js | 2 +- .../reporting/server/lib/esqueue/job.js | 2 +- .../reporting/server/routes/generation.ts | 2 - .../plugins/reporting/server/routes/legacy.ts | 73 ----- x-pack/test/reporting/README.md | 10 - .../reporting/api/bwc_existing_indexes.js | 78 ----- .../test/reporting/api/bwc_generation_urls.js | 54 ---- x-pack/test/reporting/api/chromium_tests.js | 2 - x-pack/test/reporting/api/generation_urls.js | 14 - 17 files changed, 38 insertions(+), 713 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js delete mode 100644 x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js delete mode 100644 x-pack/legacy/plugins/reporting/server/routes/legacy.ts delete mode 100644 x-pack/test/reporting/api/bwc_existing_indexes.js delete mode 100644 x-pack/test/reporting/api/bwc_generation_urls.js diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts index e25b94e98d020f..9b2a065427f70b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts @@ -28,7 +28,7 @@ test(`fails if no URL is passed`, async () => { server: mockServer, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( - `"No valid URL fields found in Job Params! Expected \`job.relativeUrl\` or \`job.objects[{ relativeUrl }]\`"` + `"No valid URL fields found in Job Params! Expected \`job.relativeUrl: string\` or \`job.relativeUrls: string[]\`"` ); }); @@ -64,7 +64,10 @@ test(`fails if URLs are file-protocols for PDF`, async () => { const relativeUrl = 'file://etc/passwd/#/something'; const fn = () => getFullUrls({ - job: { objects: [{ relativeUrl }], forceNow }, + job: { + relativeUrls: [relativeUrl], + forceNow, + }, server: mockServer, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( @@ -79,11 +82,7 @@ test(`fails if URLs are absolute for PDF`, async () => { const fn = () => getFullUrls({ job: { - objects: [ - { - relativeUrl, - }, - ], + relativeUrls: [relativeUrl], forceNow, }, server: mockServer, @@ -95,18 +94,15 @@ test(`fails if URLs are absolute for PDF`, async () => { test(`fails if any URLs are absolute or file's for PDF`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const objects = [ - { relativeUrl: '/app/kibana#/something_aaa' }, - { - relativeUrl: - 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something', - }, - { relativeUrl: 'file://etc/passwd/#/something' }, + const relativeUrls = [ + '/app/kibana#/something_aaa', + 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something', + 'file://etc/passwd/#/something', ]; const fn = () => getFullUrls({ - job: { objects, forceNow }, + job: { relativeUrls, forceNow }, server: mockServer, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( @@ -163,11 +159,11 @@ test(`adds forceNow to each of multiple urls`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const urls = await getFullUrls({ job: { - objects: [ - { relativeUrl: '/app/kibana#/something_aaa' }, - { relativeUrl: '/app/kibana#/something_bbb' }, - { relativeUrl: '/app/kibana#/something_ccc' }, - { relativeUrl: '/app/kibana#/something_ddd' }, + relativeUrls: [ + '/app/kibana#/something_aaa', + '/app/kibana#/something_bbb', + '/app/kibana#/something_ccc', + '/app/kibana#/something_ddd', ], forceNow, }, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts index 90049e8e3aea48..ca64d8632dbfeb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts @@ -20,7 +20,7 @@ function isPngJob(job: JobDocPayloadPNG | JobDocPayloadPDF): job is JobDocPayloa return (job as JobDocPayloadPNG).relativeUrl !== undefined; } function isPdfJob(job: JobDocPayloadPNG | JobDocPayloadPDF): job is JobDocPayloadPDF { - return (job as JobDocPayloadPDF).objects !== undefined; + return (job as JobDocPayloadPDF).relativeUrls !== undefined; } export function getFullUrls({ @@ -45,10 +45,10 @@ export function getFullUrls({ if (isPngJob(job)) { relativeUrls = [job.relativeUrl]; } else if (isPdfJob(job)) { - relativeUrls = job.objects.map(obj => obj.relativeUrl); + relativeUrls = job.relativeUrls; } else { throw new Error( - `No valid URL fields found in Job Params! Expected \`job.relativeUrl\` or \`job.objects[{ relativeUrl }]\`` + `No valid URL fields found in Job Params! Expected \`job.relativeUrl: string\` or \`job.relativeUrls: string[]\`` ); } diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js deleted file mode 100644 index a3afd070603b0f..00000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js +++ /dev/null @@ -1,126 +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 { uriEncode } from '../lib/uri_encode'; - -/* - * TODO: Kibana 8.0: - * Remove support for parsing Saved Object details from objectType / savedObjectId - * Including support for determining the Report title from objectType / savedObjectId - * - * - `objectType` is optional, but helps differentiate the type of report in the job listing - * - `title` must be explicitly passed - * - `relativeUrls` array OR `relativeUrl` string must be passed - */ - -const getSavedObjectTitle = async (objectType, savedObjectId, savedObjectsClient) => { - const savedObject = await savedObjectsClient.get(objectType, savedObjectId); - return savedObject.attributes.title; -}; - -const getSavedObjectRelativeUrl = (objectType, savedObjectId, queryString) => { - const appPrefixes = { - dashboard: '/dashboard/', - visualization: '/visualize/edit/', - search: '/discover/', - }; - - const appPrefix = appPrefixes[objectType]; - if (!appPrefix) throw new Error('Unexpected app type: ' + objectType); - - const hash = appPrefix + uriEncode.string(savedObjectId, true); - - return `/app/kibana#${hash}?${queryString || ''}`; -}; - -export function compatibilityShimFactory(server, logger) { - return function compatibilityShimFactory(createJobFn) { - return async function( - { - savedObjectId, // deprecating - queryString, // deprecating - browserTimezone, - objectType, - title, - relativeUrls, - layout, - }, - headers, - request - ) { - // input validation and deprecation logging - if (savedObjectId) { - if (typeof savedObjectId !== 'string') { - throw new Error('Invalid savedObjectId (deprecated). String is expected.'); - } - if (relativeUrls) { - throw new Error(`savedObjectId should not be provided if relativeUrls are provided`); - } - } else { - if (!relativeUrls) { - throw new Error(`Either relativeUrls or savedObjectId must be provided`); - } - if (!Array.isArray(relativeUrls)) { - throw new Error('Invalid relativeUrls. String[] is expected.'); - } - relativeUrls.forEach(url => { - if (typeof url !== 'string') { - throw new Error('Invalid Relative URL in relativeUrls. String is expected.'); - } - }); - } - - let kibanaRelativeUrls; - if (relativeUrls) { - kibanaRelativeUrls = relativeUrls; - } else { - kibanaRelativeUrls = [getSavedObjectRelativeUrl(objectType, savedObjectId, queryString)]; - logger.warning( - `The relativeUrls have been derived from saved object parameters. ` + - `This functionality will be removed with the next major version.` - ); - } - - let reportTitle; - try { - if (title) { - reportTitle = title; - } else { - if (objectType && savedObjectId) { - reportTitle = await getSavedObjectTitle( - objectType, - savedObjectId, - request.getSavedObjectsClient() - ); - logger.warning( - `The title has been derived from saved object parameters. This ` + - `functionality will be removed with the next major version.` - ); - } else { - logger.warning( - `A title parameter should be provided with the job generation ` + - `request. Please use Kibana to regenerate your POST URL to have a ` + - `title included in the PDF.` - ); - } - } - } catch (err) { - logger.error(err); // 404 for the savedObjectId, etc - throw err; - } - - const transformedJobParams = { - objectType, - title: reportTitle, - relativeUrls: kibanaRelativeUrls, - browserTimezone, - layout, - }; - - return await createJobFn(transformedJobParams, headers, request); - }; - }; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js deleted file mode 100644 index b3b8bca1d89555..00000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js +++ /dev/null @@ -1,297 +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 { once } from 'lodash'; -import { compatibilityShimFactory } from './compatibility_shim'; - -const createMockServer = () => { - return { - expose: jest.fn(), //fool once_per_server - log: jest.fn(), - }; -}; - -const createMockLogger = () => ({ - warning: jest.fn(), - error: jest.fn(), -}); - -const createMockRequest = () => { - return { - getSavedObjectsClient: once(function() { - return { - get: jest.fn(), - }; - }), - }; -}; - -test(`passes title through if provided`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - const title = 'test title'; - - const createJobMock = jest.fn(); - await compatibilityShim(createJobMock)( - { title, relativeUrls: ['/something'] }, - null, - createMockRequest() - ); - - expect(mockLogger.warning.mock.calls.length).toBe(0); - expect(mockLogger.error.mock.calls.length).toBe(0); - - expect(createJobMock.mock.calls.length).toBe(1); - expect(createJobMock.mock.calls[0][0].title).toBe(title); -}); - -test(`gets the title from the savedObject`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - const mockRequest = createMockRequest(); - const title = 'savedTitle'; - mockRequest.getSavedObjectsClient().get.mockReturnValue({ - attributes: { - title, - }, - }); - - await compatibilityShim(createJobMock)( - { objectType: 'search', savedObjectId: 'abc' }, - null, - mockRequest - ); - - expect(mockLogger.warning.mock.calls.length).toBe(2); - expect(mockLogger.warning.mock.calls[0][0]).toEqual( - 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' - ); - expect(mockLogger.error.mock.calls.length).toBe(0); - - expect(createJobMock.mock.calls.length).toBe(1); - expect(createJobMock.mock.calls[0][0].title).toBe(title); -}); - -test(`passes the objectType and savedObjectId to the savedObjectsClient`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - const mockRequest = createMockRequest(); - mockRequest.getSavedObjectsClient().get.mockReturnValue({ - attributes: { - title: '', - }, - }); - - const objectType = 'search'; - const savedObjectId = 'abc'; - await compatibilityShim(createJobMock)({ objectType, savedObjectId }, null, mockRequest); - - expect(mockLogger.warning.mock.calls.length).toBe(2); - expect(mockLogger.warning.mock.calls[0][0]).toEqual( - 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' - ); - expect(mockLogger.warning.mock.calls[1][0]).toEqual( - 'The title has been derived from saved object parameters. This functionality will be removed with the next major version.' - ); - expect(mockLogger.error.mock.calls.length).toBe(0); - - const getMock = mockRequest.getSavedObjectsClient().get.mock; - expect(getMock.calls.length).toBe(1); - expect(getMock.calls[0][0]).toBe(objectType); - expect(getMock.calls[0][1]).toBe(savedObjectId); -}); - -test(`logs no warnings when title and relativeUrls is passed`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - const mockRequest = createMockRequest(); - - await compatibilityShim(createJobMock)( - { title: 'Phenomenal Dashboard', relativeUrls: ['/abc', '/def'] }, - null, - mockRequest - ); - - expect(mockLogger.warning.mock.calls.length).toBe(0); - expect(mockLogger.error.mock.calls.length).toBe(0); -}); - -test(`logs warning if title can not be provided`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - const mockRequest = createMockRequest(); - await compatibilityShim(createJobMock)({ relativeUrls: ['/abc'] }, null, mockRequest); - - expect(mockLogger.warning.mock.calls.length).toBe(1); - expect(mockLogger.warning.mock.calls[0][0]).toEqual( - `A title parameter should be provided with the job generation request. Please ` + - `use Kibana to regenerate your POST URL to have a title included in the PDF.` - ); -}); - -test(`logs deprecations when generating the title/relativeUrl using the savedObject`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - const mockRequest = createMockRequest(); - mockRequest.getSavedObjectsClient().get.mockReturnValue({ - attributes: { - title: '', - }, - }); - - await compatibilityShim(createJobMock)( - { objectType: 'search', savedObjectId: 'abc' }, - null, - mockRequest - ); - - expect(mockLogger.warning.mock.calls.length).toBe(2); - expect(mockLogger.warning.mock.calls[0][0]).toEqual( - 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' - ); - expect(mockLogger.warning.mock.calls[1][0]).toEqual( - 'The title has been derived from saved object parameters. This functionality will be removed with the next major version.' - ); -}); - -test(`passes objectType through`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - const mockRequest = createMockRequest(); - - const objectType = 'foo'; - await compatibilityShim(createJobMock)( - { title: 'test', relativeUrls: ['/something'], objectType }, - null, - mockRequest - ); - - expect(mockLogger.warning.mock.calls.length).toBe(0); - expect(mockLogger.error.mock.calls.length).toBe(0); - - expect(createJobMock.mock.calls.length).toBe(1); - expect(createJobMock.mock.calls[0][0].objectType).toBe(objectType); -}); - -test(`passes the relativeUrls through`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - - const relativeUrls = ['/app/kibana#something', '/app/kibana#something-else']; - await compatibilityShim(createJobMock)({ title: 'test', relativeUrls }, null, null); - - expect(mockLogger.warning.mock.calls.length).toBe(0); - expect(mockLogger.error.mock.calls.length).toBe(0); - - expect(createJobMock.mock.calls.length).toBe(1); - expect(createJobMock.mock.calls[0][0].relativeUrls).toBe(relativeUrls); -}); - -const testSavedObjectRelativeUrl = (objectType, expectedUrl) => { - test(`generates the saved object relativeUrl for ${objectType}`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - const createJobMock = jest.fn(); - - await compatibilityShim(createJobMock)( - { title: 'test', objectType, savedObjectId: 'abc' }, - null, - null - ); - - expect(mockLogger.warning.mock.calls.length).toBe(1); - expect(mockLogger.warning.mock.calls[0][0]).toEqual( - 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' - ); - expect(mockLogger.error.mock.calls.length).toBe(0); - - expect(createJobMock.mock.calls.length).toBe(1); - expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([expectedUrl]); - }); -}; - -testSavedObjectRelativeUrl('search', '/app/kibana#/discover/abc?'); -testSavedObjectRelativeUrl('visualization', '/app/kibana#/visualize/edit/abc?'); -testSavedObjectRelativeUrl('dashboard', '/app/kibana#/dashboard/abc?'); - -test(`appends the queryString to the relativeUrl when generating from the savedObject`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - const createJobMock = jest.fn(); - - await compatibilityShim(createJobMock)( - { title: 'test', objectType: 'search', savedObjectId: 'abc', queryString: 'foo=bar' }, - null, - null - ); - - expect(mockLogger.warning.mock.calls.length).toBe(1); - expect(mockLogger.warning.mock.calls[0][0]).toEqual( - 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' - ); - expect(mockLogger.error.mock.calls.length).toBe(0); - - expect(createJobMock.mock.calls.length).toBe(1); - expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([ - '/app/kibana#/discover/abc?foo=bar', - ]); -}); - -test(`throw an Error if the objectType, savedObjectId and relativeUrls are provided`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - const createJobMock = jest.fn(); - - const promise = compatibilityShim(createJobMock)( - { - title: 'test', - objectType: 'something', - relativeUrls: ['/something'], - savedObjectId: 'abc', - }, - null, - null - ); - - await expect(promise).rejects.toBeDefined(); -}); - -test(`passes headers and request through`, async () => { - const mockLogger = createMockLogger(); - const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); - - const createJobMock = jest.fn(); - - const headers = {}; - const request = createMockRequest(); - - await compatibilityShim(createJobMock)( - { title: 'test', relativeUrls: ['/something'] }, - headers, - request - ); - - expect(mockLogger.warning.mock.calls.length).toBe(0); - expect(mockLogger.error.mock.calls.length).toBe(0); - - expect(createJobMock.mock.calls.length).toBe(1); - expect(createJobMock.mock.calls[0][1]).toBe(headers); - expect(createJobMock.mock.calls[0][2]).toBe(request); -}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts index d17713057abc1e..a8cc71175cffeb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PLUGIN_ID, PDF_JOB_TYPE } from '../../../../common/constants'; import { CreateJobFactory, ESQueueCreateJobFn, @@ -13,29 +12,16 @@ import { ConditionalHeaders, } from '../../../../types'; import { validateUrls } from '../../../../common/validate_urls'; -import { LevelLogger } from '../../../../server/lib'; import { cryptoFactory } from '../../../../server/lib/crypto'; import { JobParamsPDF } from '../../types'; -// @ts-ignore untyped module -import { compatibilityShimFactory } from './compatibility_shim'; - -interface CreateJobFnOpts { - objectType: any; - title: string; - relativeUrls: string[]; - browserTimezone: string; - layout: any; -} export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(server: ServerFacade) { - const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'create']); - const compatibilityShim = compatibilityShimFactory(server, logger); const crypto = cryptoFactory(server); - return compatibilityShim(async function createJobFn( - { objectType, title, relativeUrls, browserTimezone, layout }: CreateJobFnOpts, + return async function createJobFn( + { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, headers: ConditionalHeaders['headers'], request: RequestFacade ) { @@ -44,14 +30,14 @@ export const createJobFactory: CreateJobFactory ({ relativeUrl: u })), - headers: serializedEncryptedHeaders, - browserTimezone, - layout, basePath: request.getBasePath(), + browserTimezone, forceNow: new Date().toISOString(), + headers: serializedEncryptedHeaders, + layout, + relativeUrls, + title, + objectType, }; - }); + }; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js index db7b599a1aaabb..ddbee6e9d54a46 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js @@ -27,7 +27,8 @@ beforeEach(() => { 'server.port': 5601, }; mockServer = { - expose: () => {}, + expose: jest.fn(), + log: jest.fn(), config: memoize(() => ({ get: jest.fn() })), info: { protocol: 'http', @@ -71,7 +72,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const browserTimezone = 'UTC'; await executeJob( 'pdfJobId', - { objects: [], browserTimezone, headers: encryptedHeaders }, + { relativeUrls: [], browserTimezone, headers: encryptedHeaders }, cancellationToken ); @@ -96,7 +97,7 @@ test(`returns content_type of application/pdf`, async () => { const { content_type: contentType } = await executeJob( 'pdfJobId', - { objects: [], timeRange: {}, headers: encryptedHeaders }, + { relativeUrls: [], timeRange: {}, headers: encryptedHeaders }, cancellationToken ); expect(contentType).toBe('application/pdf'); @@ -112,7 +113,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pdfJobId', - { objects: [], timeRange: {}, headers: encryptedHeaders }, + { relativeUrls: [], timeRange: {}, headers: encryptedHeaders }, cancellationToken ); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts index b51a2d4d5711c8..0a9dcfe986ca63 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts @@ -9,7 +9,7 @@ import { JobDocPayload, ServerFacade, RequestFacade } from '../../types'; // Job params: structure of incoming user request data, after being parsed from RISON export interface JobParamsPDF { - objectType: string; + objectType: string; // visualization, dashboard, etc. Used for job info & telemetry title: string; relativeUrls: string[]; browserTimezone: string; @@ -22,7 +22,5 @@ export interface JobDocPayloadPDF extends JobDocPayload { browserTimezone: string; forceNow?: string; layout: LayoutParams; - objects: Array<{ - relativeUrl: string; - }>; + relativeUrls: string[]; } diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx index 9783372aa29c4b..320f6220aa9968 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/legacy/plugins/reporting/public/components/report_listing.tsx @@ -425,7 +425,7 @@ class ReportListingUi extends Component { return { id: job._id, type: source.jobtype, - object_type: source.payload.type, + object_type: source.payload.objectType, object_title: source.payload.title, created_by: source.created_by, created_at: source.created_at, diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js index 9e00f0447e99e4..670c2907fb8322 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js @@ -15,7 +15,7 @@ const schema = { properties: { /** * Type of object that is triggering this report. Should be either search, visualization or dashboard. - * Used for phone home stats only. + * Used for job listing and telemetry stats only. */ objectType: { type: 'text', diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js index cded6d2ce89a8c..a7d8f4df3fd541 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js @@ -57,7 +57,7 @@ export class Job extends events.EventEmitter { meta: { // We are copying these values out of payload because these fields are indexed and can be aggregated on // for tracking stats, while payload contents are not. - objectType: payload.type, + objectType: payload.objectType, layout: payload.layout ? payload.layout.id : 'none', }, payload: this.payload, diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 7bed7bc5773e45..73450b7641c8e4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -17,7 +17,6 @@ import { import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; -import { registerLegacy } from './legacy'; import { createQueueFactory, enqueueJobFactory } from '../lib'; export function registerJobGenerationRoutes( @@ -73,7 +72,6 @@ export function registerJobGenerationRoutes( } registerGenerateFromJobParams(server, handler, handleError); - registerLegacy(server, handler, handleError); // Register beta panel-action download-related API's if (config.get('xpack.reporting.csv.enablePanelActionDownload')) { diff --git a/x-pack/legacy/plugins/reporting/server/routes/legacy.ts b/x-pack/legacy/plugins/reporting/server/routes/legacy.ts deleted file mode 100644 index 011ac4a02bbf9e..00000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/legacy.ts +++ /dev/null @@ -1,73 +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 querystring from 'querystring'; -import { API_BASE_URL } from '../../common/constants'; -import { ServerFacade, RequestFacade, ReportingResponseToolkit } from '../../types'; -import { - getRouteConfigFactoryReportingPre, - GetRouteConfigFactoryFn, -} from './lib/route_config_factories'; -import { HandlerErrorFunction, HandlerFunction } from './types'; - -const getStaticFeatureConfig = (getRouteConfig: GetRouteConfigFactoryFn, featureId: string) => - getRouteConfig(() => featureId); - -const BASE_GENERATE = `${API_BASE_URL}/generate`; - -export function registerLegacy( - server: ServerFacade, - handler: HandlerFunction, - handleError: HandlerErrorFunction -) { - const getRouteConfig = getRouteConfigFactoryReportingPre(server); - - function createLegacyPdfRoute({ path, objectType }: { path: string; objectType: string }) { - const exportTypeId = 'printablePdf'; - server.route({ - path, - method: 'POST', - options: getStaticFeatureConfig(getRouteConfig, exportTypeId), - handler: async (request: RequestFacade, h: ReportingResponseToolkit) => { - const message = `The following URL is deprecated and will stop working in the next major version: ${request.url.path}`; - server.log(['warning', 'reporting', 'deprecation'], message); - - try { - const savedObjectId = request.params.savedId; - const queryString = querystring.stringify(request.query); - - return await handler( - exportTypeId, - { - objectType, - savedObjectId, - queryString, - }, - request, - h - ); - } catch (err) { - throw handleError(exportTypeId, err); - } - }, - }); - } - - createLegacyPdfRoute({ - path: `${BASE_GENERATE}/visualization/{savedId}`, - objectType: 'visualization', - }); - - createLegacyPdfRoute({ - path: `${BASE_GENERATE}/search/{savedId}`, - objectType: 'search', - }); - - createLegacyPdfRoute({ - path: `${BASE_GENERATE}/dashboard/{savedId}`, - objectType: 'dashboard', - }); -} diff --git a/x-pack/test/reporting/README.md b/x-pack/test/reporting/README.md index 30859fa96c015a..d4a6c20a835e22 100644 --- a/x-pack/test/reporting/README.md +++ b/x-pack/test/reporting/README.md @@ -82,16 +82,6 @@ node scripts/functional_tests_server.js --config test/reporting/configs/chromium **Note:** Dashboard has some snapshot testing too, in `_dashboard_snapshots.js`. This test watches for a command line flag `--updateBaselines` which automates updating the baselines. Probably worthwhile to do some similar here in the long run. - ### Adding a new BWC test - - We have tests that ensure the latest version of Kibana will continue to generate reports from URLs generated in previous versions, to ensure backward compatibility. These tests are in `api/bwc_generation_urls.js`. It's important to update these every now and then and add new ones, especially if anything in the URL changed in a release. - - To add test coverage for a specific minor release,: -1. Checkout previous branch, e.g. `git checkout upstream/6.4` -2. Sync your environment via `yarn kbn bootstrap` (Note, if you run into problems you may want to first clean via `yarn kbn clean`) -3. Start up kibana and Elasticsearch (`yarn es snapshot --license trial` in one terminal, and `yarn start` in another) -4. Load the reporting test data that is used in the tests. Ensure you are in the `x-pack` directory and run: - ``` node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana ``` diff --git a/x-pack/test/reporting/api/bwc_existing_indexes.js b/x-pack/test/reporting/api/bwc_existing_indexes.js deleted file mode 100644 index ffcf123848bb2e..00000000000000 --- a/x-pack/test/reporting/api/bwc_existing_indexes.js +++ /dev/null @@ -1,78 +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 GenerationUrls from './generation_urls'; - -/** - * This file tests the situation when a reporting index spans releases. By default reporting indexes are created - * on a weekly basis, but this is configurable so it is possible a user has this set to yearly. In that event, it - * is possible report data is getting posted to an index that was created by a very old version. We don't have a - * reporting index migration plan, so this test is important to ensure BWC, or that in the event we decide to make - * a major change in a major release, we handle it properly. - */ - -export default function({ getService }) { - const esArchiver = getService('esArchiver'); - const reportingAPI = getService('reportingAPI'); - const usageAPI = getService('usageAPI'); - - // FLAKY: https://github.com/elastic/kibana/issues/42725 - describe.skip('BWC report generation into existing indexes', () => { - let expectedCompletedReportCount; - let cleanupIndexAlias; - - describe('existing 6_2 index', () => { - before('load data and add index alias', async () => { - await reportingAPI.deleteAllReportingIndexes(); - await esArchiver.load('reporting/bwc/6_2'); - - // The index name in the 6_2 archive. - const ARCHIVED_REPORTING_INDEX = '.reporting-2018.03.11'; - cleanupIndexAlias = await reportingAPI.coerceReportsIntoExistingIndex( - ARCHIVED_REPORTING_INDEX - ); - - const stats = await usageAPI.getUsageStats(); - expectedCompletedReportCount = await reportingAPI.getCompletedReportCount(stats); - - await esArchiver.unload('reporting/bwc/6_2'); - }); - - after('remove index alias', async () => { - await cleanupIndexAlias(); - }); - - // Might not be great test practice to lump all these jobs together but reporting takes awhile and it'll be - // more efficient to post them all up front, then sequentially. - it('multiple jobs posted', async () => { - const reportPaths = []; - reportPaths.push( - await reportingAPI.postJob(GenerationUrls.CSV_DISCOVER_KUERY_AND_FILTER_6_3) - ); - reportPaths.push( - await reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_DASHBOARD_FILTER_6_3) - ); - reportPaths.push( - await reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_PIE_VISUALIZATION_6_3) - ); - reportPaths.push(await reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_6_3)); - reportPaths.push( - await reportingAPI.postJob( - GenerationUrls.PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 - ) - ); - - await reportingAPI.expectAllJobsToFinishSuccessfully(reportPaths); - }).timeout(1540000); - - it('jobs completed successfully', async () => { - const stats = await usageAPI.getUsageStats(); - expectedCompletedReportCount += 5; - reportingAPI.expectCompletedReportCount(stats, expectedCompletedReportCount); - }); - }); - }); -} diff --git a/x-pack/test/reporting/api/bwc_generation_urls.js b/x-pack/test/reporting/api/bwc_generation_urls.js deleted file mode 100644 index 25b40ff3f74a82..00000000000000 --- a/x-pack/test/reporting/api/bwc_generation_urls.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as GenerationUrls from './generation_urls'; - -export default function({ getService }) { - const reportingAPI = getService('reportingAPI'); - const usageAPI = getService('usageAPI'); - - describe('BWC report generation urls', () => { - describe('Pre 6_2', () => { - before(async () => { - await reportingAPI.deleteAllReportingIndexes(); - }); - - // The URL being tested was captured from release 6.4 and then the layout section was removed to test structure before - // preserve_layout was introduced. See https://github.com/elastic/kibana/issues/23414 - it('job posted successfully', async () => { - const path = await reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_PRE_6_2); - await reportingAPI.waitForJobToFinish(path); - const stats = await usageAPI.getUsageStats(); - reportingAPI.expectCompletedReportCount(stats, 1); - }).timeout(500000); - }); - - describe('6_2', () => { - before(async () => { - await reportingAPI.deleteAllReportingIndexes(); - }); - - // Might not be great test practice to lump all these jobs together but reporting takes awhile and it'll be - // more efficient to post them all up front, then sequentially. - it('multiple jobs posted', async () => { - const reportPaths = []; - reportPaths.push(await reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_6_2)); - reportPaths.push(await reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_VISUALIZATION_6_2)); - reportPaths.push(await reportingAPI.postJob(GenerationUrls.CSV_DISCOVER_FILTER_QUERY_6_2)); - - await reportingAPI.expectAllJobsToFinishSuccessfully(reportPaths); - }).timeout(1540000); - - it('jobs completed successfully', async () => { - const stats = await usageAPI.getUsageStats(); - reportingAPI.expectCompletedReportCount(stats, 3); - }); - }); - - // 6.3 urls currently being tested as part of the "bwc_existing_indexes" test suite. Reports are time consuming, - // don't replicate tests if we don't need to, so no specific 6_3 url tests here. - }); -} diff --git a/x-pack/test/reporting/api/chromium_tests.js b/x-pack/test/reporting/api/chromium_tests.js index 6594a80db491b1..2d5a31bb40da32 100644 --- a/x-pack/test/reporting/api/chromium_tests.js +++ b/x-pack/test/reporting/api/chromium_tests.js @@ -27,8 +27,6 @@ export default function({ loadTestFile, getService }) { await esArchiver.unload(OSS_DATA_ARCHIVE_PATH); }); - loadTestFile(require.resolve('./bwc_existing_indexes')); - loadTestFile(require.resolve('./bwc_generation_urls')); loadTestFile(require.resolve('./usage')); }); } diff --git a/x-pack/test/reporting/api/generation_urls.js b/x-pack/test/reporting/api/generation_urls.js index 182d395704a25b..b98b1a26651a15 100644 --- a/x-pack/test/reporting/api/generation_urls.js +++ b/x-pack/test/reporting/api/generation_urls.js @@ -4,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// These all have the domain name portion stripped out. The api infrastructure assumes it when we post to it anyhow. - -// The URL below was captured from release 6.4 and then the layout section was removed to test structure before -// preserve_layout was introduced. See https://github.com/elastic/kibana/issues/23414 -export const PDF_PRINT_DASHBOARD_PRE_6_2 = - '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,objectType:dashboard,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fdashboard%2F2ae34a60-3dd4-11e8-b2b9-5d5dc1715159%3F_g%3D(refreshInterval:(pause:!!t,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(description:!%27!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(),gridData:(h:15,i:!%271!%27,w:24,x:0,y:0),id:!%27145ced90-3dcb-11e8-8660-4d65aa086b3c!%27,panelIndex:!%271!%27,type:visualization,version:!%276.3.0!%27),(embeddableConfig:(),gridData:(h:15,i:!%272!%27,w:24,x:24,y:0),id:e2023110-3dcb-11e8-8660-4d65aa086b3c,panelIndex:!%272!%27,type:visualization,version:!%276.3.0!%27)),query:(language:lucene,query:!%27!%27),timeRestore:!!f,title:!%27couple%2Bpanels!%27,viewMode:view)%27),title:%27couple%20panels%27)'; - export const PDF_PRINT_DASHBOARD_6_3 = '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:dashboard,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fdashboard%2F2ae34a60-3dd4-11e8-b2b9-5d5dc1715159%3F_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(description:!%27!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(),gridData:(h:15,i:!%271!%27,w:24,x:0,y:0),id:!%27145ced90-3dcb-11e8-8660-4d65aa086b3c!%27,panelIndex:!%271!%27,type:visualization,version:!%276.3.0!%27),(embeddableConfig:(),gridData:(h:15,i:!%272!%27,w:24,x:24,y:0),id:e2023110-3dcb-11e8-8660-4d65aa086b3c,panelIndex:!%272!%27,type:visualization,version:!%276.3.0!%27)),query:(language:lucene,query:!%27!%27),timeRestore:!!f,title:!%27couple%2Bpanels!%27,viewMode:view)%27),title:%27couple%20panels%27)'; export const PDF_PRESERVE_DASHBOARD_FILTER_6_3 = @@ -21,10 +14,3 @@ export const PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 = '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:visualization,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fvisualize%2Fedit%2Fbefdb6b0-3e59-11e8-9fc3-39e49624228e%3F_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(filters:!!((!%27$state!%27:(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!%27!%27),uiState:(),vis:(aggs:!!((enabled:!!t,id:!%271!%27,params:(),schema:metric,type:count),(enabled:!!t,id:!%272!%27,params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!%271!%27,otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!%27Filter%2BTest:%2Banimals:%2Blinked%2Bto%2Bsearch%2Bwith%2Bfilter!%27,type:pie))%27),title:%27Filter%20Test:%20animals:%20linked%20to%20search%20with%20filter%27)'; export const CSV_DISCOVER_KUERY_AND_FILTER_6_3 = '/api/reporting/generate/csv?jobParams=(conflictedTypesFields:!(),fields:!(%27@timestamp%27,agent,bytes,clientip),indexPatternId:%270bf35f60-3dc9-11e8-8660-4d65aa086b3c%27,metaFields:!(_source,_id,_type,_index,_score),searchRequest:(body:(_source:(excludes:!(),includes:!(%27@timestamp%27,agent,bytes,clientip)),docvalue_fields:!(%27@timestamp%27),query:(bool:(filter:!((bool:(minimum_should_match:1,should:!((match:(clientip:%2773.14.212.83%27)))))),must:!((range:(bytes:(gte:100,lt:1000))),(range:(%27@timestamp%27:(format:epoch_millis,gte:1369165215770,lte:1526931615770)))),must_not:!(),should:!())),script_fields:(),sort:!((%27@timestamp%27:(order:desc,unmapped_type:boolean))),stored_fields:!(%27@timestamp%27,agent,bytes,clientip),version:!t),index:%27logstash-*%27),title:%27Bytes%20and%20kuery%20in%20saved%20search%20with%20filter%27,type:search)'; - -export const PDF_PRINT_DASHBOARD_6_2 = - '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:dashboard,queryString:%27_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(description:!%27!%27,filters:!!((!%27$state!%27:(store:appState),meta:(alias:!!n,disabled:!!f,field:isDog,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:isDog,negate:!!f,params:(value:!!t),type:phrase,value:true),script:(script:(inline:!%27boolean%2Bcompare(Supplier%2Bs,%2Bdef%2Bv)%2B%257Breturn%2Bs.get()%2B%253D%253D%2Bv%3B%257Dcompare(()%2B-%253E%2B%257B%2Breturn%2Bdoc%255B!!!%27animal.keyword!!!%27%255D.value%2B%253D%253D%2B!!!%27dog!!!%27%2B%257D,%2Bparams.value)%3B!%27,lang:painless,params:(value:!!t))))),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((gridData:(h:3,i:!%274!%27,w:6,x:6,y:0),id:edb65990-53ca-11e8-b481-c9426d020fcd,panelIndex:!%274!%27,type:visualization,version:!%276.2.4!%27),(gridData:(h:3,i:!%275!%27,w:6,x:0,y:0),id:!%270644f890-53cb-11e8-b481-c9426d020fcd!%27,panelIndex:!%275!%27,type:visualization,version:!%276.2.4!%27)),query:(language:lucene,query:!%27weightLbs:%253E15!%27),timeRestore:!!t,title:!%27Animal%2BWeights%2B(created%2Bin%2B6.2)!%27,viewMode:view)%27,savedObjectId:%271b2f47b0-53cb-11e8-b481-c9426d020fcd%27)'; -export const PDF_PRESERVE_VISUALIZATION_6_2 = - '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(dimensions:(height:441,width:1002),id:preserve_layout),objectType:visualization,queryString:%27_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(filters:!!(),linked:!!f,query:(language:lucene,query:!%27weightLbs:%253E10!%27),uiState:(),vis:(aggs:!!((enabled:!!t,id:!%271!%27,params:(),schema:metric,type:count),(enabled:!!t,id:!%272!%27,params:(field:weightLbs,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!%271!%27,otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!%27Weight%2Bin%2Blbs%2Bpie%2Bcreated%2Bin%2B6.2!%27,type:pie))%27,savedObjectId:%270644f890-53cb-11e8-b481-c9426d020fcd%27)'; -export const CSV_DISCOVER_FILTER_QUERY_6_2 = - '/api/reporting/generate/csv?jobParams=(conflictedTypesFields:!(),fields:!(%27@timestamp%27,animal,sound,weightLbs),indexPatternId:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,metaFields:!(_source,_id,_type,_index,_score),searchRequest:(body:(_source:(excludes:!(),includes:!(%27@timestamp%27,animal,sound,weightLbs)),docvalue_fields:!(%27@timestamp%27),query:(bool:(filter:!(),must:!((query_string:(analyze_wildcard:!t,default_field:%27*%27,query:%27weightLbs:%3E10%27)),(match_phrase:(sound.keyword:(query:growl))),(range:(%27@timestamp%27:(format:epoch_millis,gte:1523310968000,lte:1523483768000)))),must_not:!(),should:!())),script_fields:(),sort:!((%27@timestamp%27:(order:desc,unmapped_type:boolean))),stored_fields:!(%27@timestamp%27,animal,sound,weightLbs),version:!t),index:%27animals-*%27),title:%27Search%20created%20in%206.2%27,type:search)'; From 9aa23410d52b306d0550ffae6a3cfc4e22ce1eb1 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jan 2020 19:50:55 -0500 Subject: [PATCH 016/137] convert modelMemoryLimit nums to strings before validation check (#54011) --- x-pack/legacy/plugins/ml/common/util/job_utils.js | 2 +- .../hooks/use_create_analytics_form/reducer.test.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.js b/x-pack/legacy/plugins/ml/common/util/job_utils.js index 757dfbd7a9a771..8982cebed522e8 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.js @@ -521,7 +521,7 @@ export function validateModelMemoryLimitUnits(modelMemoryLimit) { let valid = true; if (modelMemoryLimit !== undefined) { - const mml = modelMemoryLimit.toUpperCase(); + const mml = String(modelMemoryLimit).toUpperCase(); const mmlSplit = mml.match(/\d+(\w+)$/); const unit = mmlSplit && mmlSplit.length === 2 ? mmlSplit[1] : null; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index fcb99ea83548dd..7ea2f74908e0e2 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -138,5 +138,11 @@ describe('useCreateAnalyticsForm', () => { validateAdvancedEditor(getMockState({ index: 'the-source-index', modelMemoryLimit: '' })) .isValid ).toBe(false); + // can still run validation check on model_memory_limit if number type + expect( + // @ts-ignore number is not assignable to type string - mml gets converted to string prior to creation + validateAdvancedEditor(getMockState({ index: 'the-source-index', modelMemoryLimit: 100 })) + .isValid + ).toBe(false); }); }); From 6398e22b4f9bbc6d01f053187e52e86738a57c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 7 Jan 2020 08:33:49 +0000 Subject: [PATCH 017/137] adding message to transaction and span metadata (#54017) --- .../__test__/SpanMetadata.test.tsx | 18 +++++++++++++----- .../MetadataTable/SpanMetadata/sections.ts | 4 +++- .../__test__/TransactionMetadata.test.tsx | 18 ++++++++++++++---- .../TransactionMetadata/sections.ts | 4 +++- .../shared/MetadataTable/sections.ts | 17 +++++++++++++++++ .../apm/typings/es_schemas/raw/SpanRaw.ts | 6 ++++++ .../typings/es_schemas/raw/TransactionRaw.ts | 6 ++++++ 7 files changed, 62 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 4b6355034f16ae..99d8a0790a816e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -31,11 +31,15 @@ describe('SpanMetadata', () => { name: 'opbeans-java' }, span: { - id: '7efbc7056b746fcb' + id: '7efbc7056b746fcb', + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } + } } } as unknown) as Span; const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent']); + expectTextsInDocument(output, ['Service', 'Agent', 'Message']); }); }); describe('when a span is presented', () => { @@ -55,11 +59,15 @@ describe('SpanMetadata', () => { response: { status_code: 200 } }, subtype: 'http', - type: 'external' + type: 'external', + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } + } } } as unknown) as Span; const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent', 'Span']); + expectTextsInDocument(output, ['Service', 'Agent', 'Span', 'Message']); }); }); describe('when there is no id inside span', () => { @@ -83,7 +91,7 @@ describe('SpanMetadata', () => { } as unknown) as Span; const output = render(, renderOptions); expectTextsInDocument(output, ['Service', 'Agent']); - expectTextsNotInDocument(output, ['Span']); + expectTextsNotInDocument(output, ['Span', 'Message']); }); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts index 7012bbcc8fceaf..5a83a9bf4ef9e1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts @@ -11,7 +11,8 @@ import { SPAN, LABELS, TRANSACTION, - TRACE + TRACE, + MESSAGE_SPAN } from '../sections'; export const SPAN_METADATA_SECTIONS: Section[] = [ @@ -20,5 +21,6 @@ export const SPAN_METADATA_SECTIONS: Section[] = [ TRANSACTION, TRACE, SERVICE, + MESSAGE_SPAN, AGENT ]; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 1fcb093fa03544..93e87e884ea766 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -35,6 +35,10 @@ function getTransaction() { notIncluded: 'transaction not included value', custom: { someKey: 'custom value' + }, + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } } } } as unknown) as Transaction; @@ -59,7 +63,8 @@ describe('TransactionMetadata', () => { 'Agent', 'URL', 'User', - 'Custom' + 'Custom', + 'Message' ]); }); @@ -81,7 +86,9 @@ describe('TransactionMetadata', () => { 'agent.someKey', 'url.someKey', 'user.someKey', - 'transaction.custom.someKey' + 'transaction.custom.someKey', + 'transaction.message.age.ms', + 'transaction.message.queue.name' ]); // excluded keys @@ -109,7 +116,9 @@ describe('TransactionMetadata', () => { 'agent value', 'url value', 'user value', - 'custom value' + 'custom value', + '1577958057123', + 'queue name' ]); // excluded values @@ -138,7 +147,8 @@ describe('TransactionMetadata', () => { 'Process', 'Agent', 'URL', - 'Custom' + 'Custom', + 'Message' ]); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts index 6b30c82bc35a06..18751efc6e1c1f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts @@ -18,7 +18,8 @@ import { PAGE, USER, USER_AGENT, - CUSTOM_TRANSACTION + CUSTOM_TRANSACTION, + MESSAGE_TRANSACTION } from '../sections'; export const TRANSACTION_METADATA_SECTIONS: Section[] = [ @@ -29,6 +30,7 @@ export const TRANSACTION_METADATA_SECTIONS: Section[] = [ CONTAINER, SERVICE, PROCESS, + MESSAGE_TRANSACTION, AGENT, URL, { ...PAGE, key: 'transaction.page' }, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts index 403663ce2095a1..ac8e9559357e3c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts @@ -136,3 +136,20 @@ export const CUSTOM_TRANSACTION: Section = { key: 'transaction.custom', label: customLabel }; + +const messageLabel = i18n.translate( + 'xpack.apm.metadataTable.section.messageLabel', + { + defaultMessage: 'Message' + } +); + +export const MESSAGE_TRANSACTION: Section = { + key: 'transaction.message', + label: messageLabel +}; + +export const MESSAGE_SPAN: Section = { + key: 'span.message', + label: messageLabel +}; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts index 5ba480221c997e..60e523f1aa0430 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts @@ -40,6 +40,12 @@ export interface SpanRaw extends APMBaseDoc { statement?: string; type?: string; }; + message?: { + queue?: { name: string }; + age?: { ms: number }; + body?: string; + headers?: Record; + }; }; transaction?: { id: string; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts index ce7c11f34a220a..4dc5f8c897c265 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts @@ -43,6 +43,12 @@ export interface TransactionRaw extends APMBaseDoc { }; type: string; custom?: Record; + message?: { + queue?: { name: string }; + age?: { ms: number }; + body?: string; + headers?: Record; + }; }; // Shared by errors and transactions From e687fc63dfe815877378299f2b9f7c443ccf6088 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 7 Jan 2020 11:01:21 +0100 Subject: [PATCH 018/137] [Console] Telemetry (part 1) (#52893) * Saving anonymised data to SO * Add new files * Hook up usage collector * Added app start up ui metric tracking * Only use client side track metrics functionality * Added comment regarding use of `patterns`, renamed trackMetric -> trackUiMetric * Fix jest tests * Slight refactor and fix for functional tests. More defensive tracking logic * Fix types in test * Minor refactor to get endpoint description - removed SenseEditor from autocomplete. Fix bug where cursor at end of line does not get endpoint informaiton * Send request to es: do not mutate args Always move cursor to end of line when getting endpoint description * Create an interface a simple interface to the metrics tracker Use the new createUiStatsReporter function to create the tracker Co-authored-by: Elastic Machine --- .../core_plugins/console/public/kibana.json | 3 +- .../core_plugins/console/public/legacy.ts | 10 +++-- .../console_editor/editor.test.mock.tsx | 4 ++ .../legacy/console_editor/editor.test.tsx | 16 ++++++-- .../editor/legacy/console_menu_actions.ts | 5 +-- .../np_ready/application/contexts/index.ts | 2 +- .../application/contexts/services_context.tsx | 4 +- .../send_request_to_es.ts | 3 +- .../use_send_current_request_to_es/track.ts | 40 ++++++++++++++++++ .../use_send_current_request_to_es.ts | 16 ++++---- .../public/np_ready/application/index.tsx | 12 +++++- .../application/models/sense_editor/index.ts | 1 + .../np_ready/lib/autocomplete/autocomplete.ts | 35 ++++++---------- .../get_endpoint_from_position.ts | 41 +++++++++++++++++++ .../console/public/np_ready/lib/es/es.js | 1 - .../public/np_ready/services/tracker.ts | 31 ++++++++++++++ .../console/public/np_ready/types/common.ts | 5 +++ .../console/public/np_ready/types/index.ts | 1 + .../spec/overrides/sql.query.json | 3 +- 19 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts create mode 100644 src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts create mode 100644 src/legacy/core_plugins/console/public/np_ready/services/tracker.ts diff --git a/src/legacy/core_plugins/console/public/kibana.json b/src/legacy/core_plugins/console/public/kibana.json index 3363af353912ac..c58a5a90fb9f28 100644 --- a/src/legacy/core_plugins/console/public/kibana.json +++ b/src/legacy/core_plugins/console/public/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["home"] + "requiredPlugins": ["home"], + "optionalPlugins": ["usageCollection"] } diff --git a/src/legacy/core_plugins/console/public/legacy.ts b/src/legacy/core_plugins/console/public/legacy.ts index c456d777187aad..d151a27d27e5cc 100644 --- a/src/legacy/core_plugins/console/public/legacy.ts +++ b/src/legacy/core_plugins/console/public/legacy.ts @@ -22,7 +22,13 @@ import { I18nContext } from 'ui/i18n'; import chrome from 'ui/chrome'; import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import { plugin } from './np_ready'; +import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; +import { HomePublicPluginSetup } from '../../../../plugins/home/public'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; + export interface XPluginSet { + usageCollection: UsageCollectionSetup; dev_tools: DevToolsSetup; home: HomePublicPluginSetup; __LEGACY: { @@ -32,10 +38,6 @@ export interface XPluginSet { }; } -import { plugin } from './np_ready'; -import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; -import { HomePublicPluginSetup } from '../../../../plugins/home/public'; - const pluginInstance = plugin({} as any); (async () => { diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx index 5df72c0f034966..0ee7998d331f59 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx @@ -22,6 +22,7 @@ jest.mock('../../../../contexts/editor_context/editor_registry.ts', () => ({ setInputEditor: () => {}, getInputEditor: () => ({ getRequestsInRange: async () => [{ test: 'test' }], + getCoreEditor: () => ({ getCurrentPosition: jest.fn() }), }), }, })); @@ -52,3 +53,6 @@ jest.mock('../../../../models/sense_editor', () => { jest.mock('../../../../hooks/use_send_current_request_to_es/send_request_to_es', () => ({ sendRequestToES: jest.fn(), })); +jest.mock('../../../../../lib/autocomplete/get_endpoint_from_position', () => ({ + getEndpointFromPosition: jest.fn(), +})); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx index 6162397ce0650e..73ee6d160613f8 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -32,14 +32,18 @@ import { ServicesContextProvider, EditorContextProvider, RequestContextProvider, + ContextValue, } from '../../../../contexts'; +// Mocked functions import { sendRequestToES } from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; +import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; + import * as consoleMenuActions from '../console_menu_actions'; import { Editor } from './editor'; describe('Legacy (Ace) Console Editor Component Smoke Test', () => { - let mockedAppContextValue: any; + let mockedAppContextValue: ContextValue; const sandbox = sinon.createSandbox(); const doMount = () => @@ -58,11 +62,15 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { beforeEach(() => { document.queryCommandSupported = sinon.fake(() => true); mockedAppContextValue = { + elasticsearchUrl: 'test', services: { + trackUiMetric: { count: () => {}, load: () => {} }, + settings: {} as any, + storage: {} as any, history: { - getSavedEditorState: () => null, + getSavedEditorState: () => ({} as any), updateCurrentState: jest.fn(), - }, + } as any, notifications: notificationServiceMock.createSetupContract(), }, docLinkVersion: 'NA', @@ -70,10 +78,12 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { }); afterEach(() => { + jest.clearAllMocks(); sandbox.restore(); }); it('calls send current request to ES', async () => { + (getEndpointFromPosition as jest.Mock).mockReturnValue({ patterns: [] }); (sendRequestToES as jest.Mock).mockRejectedValue({}); const editor = doMount(); act(() => { diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts index 797ff5744eec31..2bbe49cd53eace 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts @@ -17,8 +17,7 @@ * under the License. */ -// @ts-ignore -import { getEndpointFromPosition } from '../../../../lib/autocomplete/autocomplete'; +import { getEndpointFromPosition } from '../../../../lib/autocomplete/get_endpoint_from_position'; import { SenseEditor } from '../../../models/sense_editor'; export async function autoIndent(editor: SenseEditor, event: Event) { @@ -40,7 +39,7 @@ export function getDocumentation( } const position = requests[0].range.end; position.column = position.column - 1; - const endpoint = getEndpointFromPosition(editor, position, editor.parser); + const endpoint = getEndpointFromPosition(editor.getCoreEditor(), position, editor.parser); if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) { return endpoint.documentation .replace('/master/', `/${docLinkVersion}/`) diff --git a/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts index 18234acf15957a..e489bd50c9ce05 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { useServicesContext, ServicesContextProvider } from './services_context'; +export { useServicesContext, ServicesContextProvider, ContextValue } from './services_context'; export { useRequestActionContext, diff --git a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx index 77f0924a51842d..f14685ecd4ac74 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx @@ -20,13 +20,15 @@ import React, { createContext, useContext } from 'react'; import { NotificationsSetup } from 'kibana/public'; import { History, Storage, Settings } from '../../services'; +import { MetricsTracker } from '../../types'; -interface ContextValue { +export interface ContextValue { services: { history: History; storage: Storage; settings: Settings; notifications: NotificationsSetup; + trackUiMetric: MetricsTracker; }; elasticsearchUrl: string; docLinkVersion: string; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 11c1f6638e9cf5..10dab65b61d44a 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -39,7 +39,8 @@ export interface ESRequestResult { } let CURRENT_REQ_ID = 0; -export function sendRequestToES({ requests }: EsRequestArgs): Promise { +export function sendRequestToES(args: EsRequestArgs): Promise { + const requests = args.requests.slice(); return new Promise((resolve, reject) => { const reqId = ++CURRENT_REQ_ID; const results: ESRequestResult[] = []; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts new file mode 100644 index 00000000000000..4d993512c8fa72 --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts @@ -0,0 +1,40 @@ +/* + * 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 { SenseEditor } from '../../models/sense_editor'; +import { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; +import { MetricsTracker } from '../../../types'; + +export const track = (requests: any[], editor: SenseEditor, trackUiMetric: MetricsTracker) => { + const coreEditor = editor.getCoreEditor(); + // `getEndpointFromPosition` gets values from the server-side generated JSON files which + // are a combination of JS, automatically generated JSON and manual overrides. That means + // the metrics reported from here will be tied to the definitions in those files. + // See src/legacy/core_plugins/console/server/api_server/spec + const endpointDescription = getEndpointFromPosition( + coreEditor, + coreEditor.getCurrentPosition(), + editor.parser + ); + + if (requests[0] && endpointDescription) { + const eventName = `${requests[0].method}_${endpointDescription.id ?? 'unknown'}`; + trackUiMetric.count(eventName); + } +}; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index b51c29f8e9db6c..6bf0b5024376bf 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -19,15 +19,16 @@ import { i18n } from '@kbn/i18n'; import { useCallback } from 'react'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; -import { useServicesContext } from '../../contexts'; +import { useRequestActionContext, useServicesContext } from '../../contexts'; import { sendRequestToES } from './send_request_to_es'; -import { useRequestActionContext } from '../../contexts'; +import { track } from './track'; + // @ts-ignore import mappings from '../../../lib/mappings/mappings'; export const useSendCurrentRequestToES = () => { const { - services: { history, settings, notifications }, + services: { history, settings, notifications, trackUiMetric }, } = useServicesContext(); const dispatch = useRequestActionContext(); @@ -45,9 +46,10 @@ export const useSendCurrentRequestToES = () => { return; } - const results = await sendRequestToES({ - requests, - }); + // Fire and forget + setTimeout(() => track(requests, editor, trackUiMetric), 0); + + const results = await sendRequestToES({ requests }); results.forEach(({ request: { path, method, data } }) => { history.addToHistory(path, method, data); @@ -82,5 +84,5 @@ export const useSendCurrentRequestToES = () => { }); } } - }, [dispatch, settings, history, notifications]); + }, [dispatch, settings, history, notifications, trackUiMetric]); }; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx index 239e4320f00f8f..89756513b2b22c 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx @@ -22,6 +22,7 @@ import { NotificationsSetup } from 'kibana/public'; import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings, Settings } from '../services'; +import { createUsageTracker } from '../services/tracker'; let settingsRef: Settings; export function legacyBackDoorToSettings() { @@ -36,6 +37,9 @@ export function boot(deps: { }) { const { I18nContext, notifications, docLinkVersion, elasticsearchUrl } = deps; + const trackUiMetric = createUsageTracker(); + trackUiMetric.load('opened_app'); + const storage = createStorage({ engine: window.localStorage, prefix: 'sense:', @@ -50,7 +54,13 @@ export function boot(deps: { value={{ elasticsearchUrl, docLinkVersion, - services: { storage, history, settings, notifications }, + services: { + storage, + history, + settings, + notifications, + trackUiMetric, + }, }} > diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts index 9310de2724fbef..f2102d75685fd4 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts @@ -21,3 +21,4 @@ export * from './create'; export * from '../legacy_core_editor/create_readonly'; export { MODE } from '../../../lib/row_parser'; export { SenseEditor } from './sense_editor'; +export { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts index 7520807ca77f58..ac8fa1ea48caae 100644 --- a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts +++ b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts @@ -38,7 +38,6 @@ import { URL_PATH_END_MARKER } from './components/index'; import { createTokenIterator } from '../../application/factories'; import { Position, Token, Range, CoreEditor } from '../../types'; -import { SenseEditor } from '../../application/models/sense_editor'; let LAST_EVALUATED_TOKEN: any = null; @@ -54,11 +53,20 @@ function isUrlParamsToken(token: any) { return false; } } -function getCurrentMethodAndTokenPaths( + +/** + * Get the method and token paths for a specific position in the current editor buffer. + * + * This function can be used for getting autocomplete information or for getting more information + * about the endpoint associated with autocomplete. In future, these concerns should be better + * separated. + * + */ +export function getCurrentMethodAndTokenPaths( editor: CoreEditor, pos: Position, parser: any, - forceEndOfUrl?: boolean + forceEndOfUrl?: boolean /* Flag for indicating whether we want to avoid early escape optimization. */ ) { const tokenIter = createTokenIterator({ editor, @@ -186,7 +194,7 @@ function getCurrentMethodAndTokenPaths( } } - if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0)) { + if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0) && !forceEndOfUrl) { // we had some content and still no path -> the cursor is position after a closed body -> no auto complete return {}; } @@ -298,20 +306,6 @@ function getCurrentMethodAndTokenPaths( } return ret; } -export function getEndpointFromPosition(senseEditor: SenseEditor, pos: Position, parser: any) { - const editor = senseEditor.getCoreEditor(); - const context = { - ...getCurrentMethodAndTokenPaths( - editor, - { column: pos.column, lineNumber: pos.lineNumber }, - parser, - true - ), - }; - const components = getTopLevelUrlCompleteComponents(context.method); - populateContext(context.urlTokenPath, context, editor, true, components); - return context.endpoint; -} // eslint-disable-next-line export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor; parser: any }) { @@ -812,7 +806,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!ret.urlTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid url token path."); return context; } @@ -825,13 +818,11 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor ); if (!context.endpoint) { - // console.log("couldn't resolve an endpoint."); return context; } if (!ret.urlParamsTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid urlParams token path."); return context; } let tokenPath: any[] = []; @@ -859,7 +850,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor context.requestStartRow = ret.requestStartRow; if (!ret.urlTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid url token path."); return context; } @@ -875,7 +865,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!ret.bodyTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid body token path."); return context; } diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts new file mode 100644 index 00000000000000..cb037e29e33f64 --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts @@ -0,0 +1,41 @@ +/* + * 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 { CoreEditor, Position } from '../../types'; +import { getCurrentMethodAndTokenPaths } from './autocomplete'; + +// @ts-ignore +import { getTopLevelUrlCompleteComponents } from '../kb/kb'; +// @ts-ignore +import { populateContext } from './engine'; + +export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: any) { + const lineValue = editor.getLineValue(pos.lineNumber); + const context = { + ...getCurrentMethodAndTokenPaths( + editor, + { column: lineValue.length, lineNumber: pos.lineNumber }, + parser, + true + ), + }; + const components = getTopLevelUrlCompleteComponents(context.method); + populateContext(context.urlTokenPath, context, editor, true, components); + return context.endpoint; +} diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js b/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js index 9012b875e0f2be..e36976fb7acee7 100644 --- a/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js +++ b/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js @@ -18,7 +18,6 @@ */ import { stringify as formatQueryString } from 'querystring'; - import $ from 'jquery'; const esVersion = []; diff --git a/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts b/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts new file mode 100644 index 00000000000000..13d5f875b3c6fb --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts @@ -0,0 +1,31 @@ +/* + * 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 { METRIC_TYPE } from '@kbn/analytics'; +import { MetricsTracker } from '../types'; +import { createUiStatsReporter } from '../../../../ui_metric/public'; + +const APP_TRACKER_NAME = 'console'; +export const createUsageTracker = (): MetricsTracker => { + const track = createUiStatsReporter(APP_TRACKER_NAME); + return { + count: (eventName: string) => track(METRIC_TYPE.COUNT, eventName), + load: (eventName: string) => track(METRIC_TYPE.LOADED, eventName), + }; +}; diff --git a/src/legacy/core_plugins/console/public/np_ready/types/common.ts b/src/legacy/core_plugins/console/public/np_ready/types/common.ts index ad9ed10d4188fb..e44969cd9e80a9 100644 --- a/src/legacy/core_plugins/console/public/np_ready/types/common.ts +++ b/src/legacy/core_plugins/console/public/np_ready/types/common.ts @@ -17,6 +17,11 @@ * under the License. */ +export interface MetricsTracker { + count: (eventName: string) => void; + load: (eventName: string) => void; +} + export type BaseResponseType = | 'application/json' | 'text/csv' diff --git a/src/legacy/core_plugins/console/public/np_ready/types/index.ts b/src/legacy/core_plugins/console/public/np_ready/types/index.ts index 9d82237d667b3a..78c6b6c8f55cc3 100644 --- a/src/legacy/core_plugins/console/public/np_ready/types/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/types/index.ts @@ -20,3 +20,4 @@ export * from './core_editor'; export * from './token'; export * from './tokens_provider'; +export * from './common'; diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json index 843fba30bb4899..c78cfeea8473d7 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json +++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json @@ -19,6 +19,7 @@ "smile" ] }, - "template": "_sql?format=json\n{\n \"query\": \"\"\"\n SELECT * FROM \"${1:TABLE}\"\n \"\"\"\n}\n" + "template": "_sql?format=json\n{\n \"query\": \"\"\"\n SELECT * FROM \"${1:TABLE}\"\n \"\"\"\n}\n", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest-overview.html" } } From 7607c162fe0b464052d0324580e589c5a84e4ea3 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 7 Jan 2020 12:53:58 +0100 Subject: [PATCH 019/137] removes logout (#54098) --- .../siem/cypress/integration/lib/logout/index.ts | 15 --------------- .../cypress/integration/lib/logout/selectors.ts | 11 ----------- .../events_viewer/events_viewer.spec.ts | 5 ----- .../fields_browser/fields_browser.spec.ts | 5 ----- .../smoke_tests/inspect/inspect.spec.ts | 8 -------- .../ml_conditional_links.spec.ts | 5 ----- .../smoke_tests/navigation/navigation.spec.ts | 5 ----- .../smoke_tests/overview/overview.spec.ts | 5 ----- .../smoke_tests/pagination/pagination.spec.ts | 5 ----- .../smoke_tests/timeline/data_providers.spec.ts | 5 ----- .../smoke_tests/timeline/flyout_button.spec.ts | 5 ----- .../smoke_tests/timeline/search_or_filter.spec.ts | 5 ----- .../smoke_tests/timeline/toggle_column.spec.ts | 5 ----- .../smoke_tests/url_state/url_state.spec.ts | 5 ----- 14 files changed, 89 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts delete mode 100644 x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts deleted file mode 100644 index 7a6c7f71bc98c2..00000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const logout = (): null => { - cy.request({ - method: 'GET', - url: `${Cypress.config().baseUrl}/logout`, - }).then(response => { - expect(response.status).to.eq(200); - }); - return null; -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts deleted file mode 100644 index 8cf015619f4c1f..00000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts +++ /dev/null @@ -1,11 +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. - */ - -/** The avatar / button that represents the logged-in Kibana user */ -export const USER_MENU = '[data-test-subj="userMenuButton"]'; - -/** Clicking this link logs out the currently logged-in Kibana user */ -export const LOGOUT_LINK = '[data-test-subj="logoutLink"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts index 85878d82256096..79169d3769a781 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts @@ -10,7 +10,6 @@ import { FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, FIELDS_BROWSER_TITLE, } from '../../lib/fields_browser/selectors'; -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; import { @@ -46,10 +45,6 @@ describe('Events Viewer', () => { clickEventsTab(); }); - afterEach(() => { - return logout(); - }); - it('renders the fields browser with the expected title when the Events Viewer Fields Browser button is clicked', () => { openEventsViewerFieldsBrowser(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts index dfc5e10893ebbc..95df907893fc7b 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts @@ -22,7 +22,6 @@ import { FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_TITLE, } from '../../lib/fields_browser/selectors'; -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; @@ -42,10 +41,6 @@ describe('Fields Browser', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('renders the fields browser with the expected title when the Fields button is clicked', () => { populateTimeline(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts index 54207966fd36f2..ee25705a83989d 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { INSPECT_BUTTON_ICON, @@ -18,9 +17,6 @@ import { executeKQL, hostExistsQuery, toggleTimelineVisibility } from '../../lib describe('Inspect', () => { describe('Hosts and network stats and tables', () => { - afterEach(() => { - return logout(); - }); INSPECT_BUTTONS_IN_SIEM.map(table => it(`inspects the ${table.title}`, () => { loginAndWaitForPage(table.url); @@ -36,10 +32,6 @@ describe('Inspect', () => { }); describe('Timeline', () => { - afterEach(() => { - return logout(); - }); - it('inspects the timeline', () => { loginAndWaitForPage(HOSTS_PAGE); toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index 4c29c081b3e69e..afeb8c3c13a4fd 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { mlNetworkSingleIpNullKqlQuery, mlNetworkSingleIpKqlQuery, @@ -24,10 +23,6 @@ import { loginAndWaitForPage } from '../../lib/util/helpers'; import { KQL_INPUT } from '../../lib/url_state'; describe('ml conditional links', () => { - afterEach(() => { - return logout(); - }); - it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts index f4beba7cbb72d8..bb1a0379ce0eaf 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { OVERVIEW_PAGE, TIMELINES_PAGE } from '../../lib/urls'; import { NAVIGATION_HOSTS, @@ -15,10 +14,6 @@ import { import { loginAndWaitForPage } from '../../lib/util/helpers'; describe('top-level navigation common to all pages in the SIEM app', () => { - afterEach(() => { - return logout(); - }); - it('navigates to the Overview page', () => { loginAndWaitForPage(TIMELINES_PAGE); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts index 2ea8b5e8bc5ce7..4ef3eb67cafc9c 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { OVERVIEW_PAGE } from '../../lib/urls'; import { clearFetch, stubApi } from '../../lib/fixtures/helpers'; import { HOST_STATS, NETWORK_STATS, STAT_AUDITD } from '../../lib/overview/selectors'; @@ -17,10 +16,6 @@ describe('Overview Page', () => { loginAndWaitForPage(OVERVIEW_PAGE); }); - afterEach(() => { - return logout(); - }); - it('Host and Network stats render with correct values', () => { cy.get(STAT_AUDITD.domId); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts index ebd0ad0125efb6..73711f1434d5ff 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { HOSTS_PAGE_TAB_URLS } from '../../lib/urls'; import { AUTHENTICATIONS_TABLE, @@ -19,10 +18,6 @@ import { import { DEFAULT_TIMEOUT, loginAndWaitForPage, waitForTableLoad } from '../../lib/util/helpers'; describe('Pagination', () => { - afterEach(() => { - return logout(); - }); - it('pagination updates results and page number', () => { loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 236d5a53481b7f..824e4031852383 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { TIMELINE_DATA_PROVIDERS, TIMELINE_DROPPED_DATA_PROVIDERS, @@ -22,10 +21,6 @@ describe('timeline data providers', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { waitForAllHostsWidget(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts index c1c35e497d0815..5b0ac03ae87dc0 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON, @@ -21,10 +20,6 @@ describe('timeline flyout button', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('toggles open the timeline', () => { toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts index 0c9aed33d47ad6..9f21b4e3d53a11 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { assertAtLeastOneEventMatchesSearch, executeKQL, @@ -19,10 +18,6 @@ describe('timeline search or filter KQL bar', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('executes a KQL query', () => { toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 8197f77db9a081..9a915b0e77d440 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -6,7 +6,6 @@ import { drag, drop } from '../../lib/drag_n_drop/helpers'; import { populateTimeline } from '../../lib/fields_browser/helpers'; -import { logout } from '../../lib/logout'; import { toggleFirstTimelineEventDetails } from '../../lib/timeline/helpers'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; @@ -16,10 +15,6 @@ describe('toggle column in timeline', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - const timestampField = '@timestamp'; const idField = '_id'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts index dba5099a93c5a1..33ee2cb1cb302f 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { ABSOLUTE_DATE_RANGE, DATE_PICKER_ABSOLUTE_INPUT, @@ -33,10 +32,6 @@ import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; import { NAVIGATION_HOSTS_ALL_HOSTS, NAVIGATION_HOSTS_ANOMALIES } from '../../lib/hosts/selectors'; describe('url state', () => { - afterEach(() => { - return logout(); - }); - it('sets the global start and end dates from the url', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( From 58cb24a7e66f5e740e53ad5a16bb2ced4a98ca2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 7 Jan 2020 11:56:23 +0000 Subject: [PATCH 020/137] [APM] Show errors on the timeline instead of under the transaction (#53756) * creating error marker and refactoring some stuff * styling popover * adding agent marks and errors to waterfall items * adding agent marks and errors to waterfall items * adding agent marks and errors to waterfall items * fixing tests and typescript checking * refactoring helper * changing transaction error badge style * adding unit test * fixing agent marker position * fixing offset when error is registered before its parent * refactoring error marker * refactoring error marker * refactoring error marker * refactoring error marker * refactoring error marker * refactoring waterfall helper * refactoring waterfall helper * refactoring waterfall helper api * refactoring waterfall helper * removing unused code * refactoring waterfall helper * changing unit test * removing comment * refactoring marker component and waterfall helper * removing servicecolor from waterfall item and adding it to errormark * fixing trace order --- .../WaterfallWithSummmary/ErrorCount.tsx | 32 + .../WaterfallWithSummmary/ErrorCountBadge.tsx | 17 - .../MaybeViewTraceLink.tsx | 17 +- .../WaterfallWithSummmary/TransactionTabs.tsx | 1 - .../__test__}/get_agent_marks.test.ts | 25 +- .../Marks/__test__/get_error_marks.test.ts | 97 + .../Marks/get_agent_marks.ts | 31 + .../Marks/get_error_marks.ts | 39 + .../WaterfallContainer/Marks/index.ts | 12 + .../WaterfallContainer/ServiceLegends.tsx | 2 +- .../Waterfall/SpanFlyout/index.tsx | 2 +- .../Waterfall/TransactionFlyout/index.tsx | 10 +- .../Waterfall/WaterfallFlyout.tsx | 59 + .../Waterfall/WaterfallItem.tsx | 83 +- .../WaterfallContainer/Waterfall/index.tsx | 216 +- .../waterfall_helpers.test.ts.snap | 1771 +++++++++++------ .../waterfall_helpers.test.ts | 241 ++- .../waterfall_helpers/waterfall_helpers.ts | 254 ++- .../WaterfallContainer/get_agent_marks.ts | 27 - .../WaterfallContainer/index.tsx | 9 +- .../__tests__/ErrorCount.test.tsx | 37 + .../WaterfallWithSummmary/index.tsx | 27 +- .../components/shared/Links/apm/APMLink.tsx | 2 +- ...tem.tsx => ErrorCountSummaryItemBadge.tsx} | 27 +- .../shared/Summary/TransactionSummary.tsx | 4 +- .../ErrorCountSummaryItemBadge.test.tsx | 21 + .../shared/charts/CustomPlot/Legends.js | 2 +- .../__snapshots__/CustomPlot.test.js.snap | 27 + .../components/shared/charts/Legend/index.js | 56 - .../components/shared/charts/Legend/index.tsx | 94 + .../AgentMarker.tsx} | 40 +- .../charts/Timeline/Marker/ErrorMarker.tsx | 103 + .../Marker/__test__/AgentMarker.test.tsx | 23 + .../Marker/__test__/ErrorMarker.test.tsx | 30 + .../Timeline/Marker/__test__/Marker.test.tsx | 42 + .../__snapshots__/AgentMarker.test.tsx.snap | 26 + .../__snapshots__/ErrorMarker.test.tsx.snap | 49 + .../__snapshots__/Marker.test.tsx.snap | 58 + .../shared/charts/Timeline/Marker/index.tsx | 36 + .../shared/charts/Timeline/TimelineAxis.js | 16 +- .../shared/charts/Timeline/VerticalLines.js | 12 +- .../charts/Timeline/__test__/Timeline.test.js | 23 +- .../__snapshots__/Timeline.test.js.snap | 197 +- .../shared/charts/Timeline/index.js | 8 +- .../components/shared/charts/Tooltip/index.js | 2 +- .../get_trace_errors_per_transaction.ts | 1 - .../traces/__snapshots__/queries.test.ts.snap | 1 + .../apm/server/lib/traces/get_trace_items.ts | 5 +- 48 files changed, 2575 insertions(+), 1339 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx rename x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/{ => Marks/__test__}/get_agent_marks.test.ts (66%) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx rename x-pack/legacy/plugins/apm/public/components/shared/Summary/{ErrorCountSummaryItem.tsx => ErrorCountSummaryItemBadge.tsx} (51%) create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx rename x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/{AgentMarker.js => Marker/AgentMarker.tsx} (56%) create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx new file mode 100644 index 00000000000000..ff2cb69d011fa4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.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 { EuiText, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +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 } + })} + +

+
+); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx deleted file mode 100644 index 4c3ec3ca9f3080..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.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 { EuiBadge } from '@elastic/eui'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import React from 'react'; - -type Props = React.ComponentProps; - -export const ErrorCountBadge = ({ children, ...rest }: Props) => ( - - {children} - -); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 39e52be34a415a..322ec7c422571a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -25,8 +25,9 @@ export const MaybeViewTraceLink = ({ } ); + const { rootTransaction } = waterfall; // the traceroot cannot be found, so we cannot link to it - if (!waterfall.traceRoot) { + if (!rootTransaction) { return ( {viewFullTraceButtonLabel} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index e5be12509e3c91..f8318b9ae97e6d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -77,7 +77,6 @@ export function TransactionTabs({ {currentTab.key === timelineTab.key ? ( { it('should sort the marks by time', () => { @@ -21,9 +21,24 @@ describe('getAgentMarks', () => { } } as any; expect(getAgentMarks(transaction)).toEqual([ - { name: 'timeToFirstByte', us: 10000 }, - { name: 'domInteractive', us: 117000 }, - { name: 'domComplete', us: 118000 } + { + id: 'timeToFirstByte', + offset: 10000, + type: 'agentMark', + verticalLine: true + }, + { + id: 'domInteractive', + offset: 117000, + type: 'agentMark', + verticalLine: true + }, + { + id: 'domComplete', + offset: 118000, + type: 'agentMark', + verticalLine: true + } ]); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts new file mode 100644 index 00000000000000..8fd8edd7f8a728 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IWaterfallItem } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; +import { getErrorMarks } from '../get_error_marks'; + +describe('getErrorMarks', () => { + describe('returns empty array', () => { + it('when items are missing', () => { + expect(getErrorMarks([], {})).toEqual([]); + }); + it('when any error is available', () => { + const items = [ + { docType: 'span' }, + { docType: 'transaction' } + ] as IWaterfallItem[]; + expect(getErrorMarks(items, {})).toEqual([]); + }); + }); + + it('returns error marks', () => { + const items = [ + { + docType: 'error', + offset: 10, + skew: 5, + doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } + } as unknown, + { docType: 'transaction' }, + { + docType: 'error', + offset: 50, + skew: 0, + doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } + } as unknown + ] as IWaterfallItem[]; + expect( + getErrorMarks(items, { 'opbeans-java': 'red', 'opbeans-node': 'blue' }) + ).toEqual([ + { + type: 'errorMark', + offset: 15, + verticalLine: false, + id: 1, + error: { error: { id: 1 }, service: { name: 'opbeans-java' } }, + serviceColor: 'red' + }, + { + type: 'errorMark', + offset: 50, + verticalLine: false, + id: 2, + error: { error: { id: 2 }, service: { name: 'opbeans-node' } }, + serviceColor: 'blue' + } + ]); + }); + + it('returns error marks without service color', () => { + const items = [ + { + docType: 'error', + offset: 10, + skew: 5, + doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } + } as unknown, + { docType: 'transaction' }, + { + docType: 'error', + offset: 50, + skew: 0, + doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } + } as unknown + ] as IWaterfallItem[]; + expect(getErrorMarks(items, {})).toEqual([ + { + type: 'errorMark', + offset: 15, + verticalLine: false, + id: 1, + error: { error: { id: 1 }, service: { name: 'opbeans-java' } }, + serviceColor: undefined + }, + { + type: 'errorMark', + offset: 50, + verticalLine: false, + id: 2, + error: { error: { id: 2 }, service: { name: 'opbeans-node' } }, + serviceColor: undefined + } + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts new file mode 100644 index 00000000000000..7798d716cb2194 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.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 { sortBy } from 'lodash'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/Transaction'; +import { Mark } from '.'; + +// Extends Mark without adding new properties to it. +export interface AgentMark extends Mark { + type: 'agentMark'; +} + +export function getAgentMarks(transaction?: Transaction): AgentMark[] { + const agent = transaction?.transaction.marks?.agent; + if (!agent) { + return []; + } + + return sortBy( + Object.entries(agent).map(([name, ms]) => ({ + type: 'agentMark', + id: name, + offset: ms * 1000, + verticalLine: true + })), + 'offset' + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts new file mode 100644 index 00000000000000..f1f0163a49d105 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts @@ -0,0 +1,39 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/ErrorRaw'; +import { + IWaterfallItem, + IWaterfallError, + IServiceColors +} from '../Waterfall/waterfall_helpers/waterfall_helpers'; +import { Mark } from '.'; + +export interface ErrorMark extends Mark { + type: 'errorMark'; + error: ErrorRaw; + serviceColor?: string; +} + +export const getErrorMarks = ( + items: IWaterfallItem[], + serviceColors: IServiceColors +): ErrorMark[] => { + if (isEmpty(items)) { + return []; + } + + return (items.filter( + item => item.docType === 'error' + ) as IWaterfallError[]).map(error => ({ + type: 'errorMark', + offset: error.offset + error.skew, + verticalLine: false, + id: error.doc.error.id, + error: error.doc, + serviceColor: serviceColors[error.doc.service.name] + })); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts new file mode 100644 index 00000000000000..52f811f5c3969d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.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. + */ + +export interface Mark { + type: string; + offset: number; + verticalLine: boolean; + id: string; +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx index e4cb4ff62b36c6..4e6a0eaf455852 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx @@ -10,7 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { px, unit } from '../../../../../style/variables'; // @ts-ignore -import Legend from '../../../../shared/charts/Legend'; +import { Legend } from '../../../../shared/charts/Legend'; import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers'; const Legends = styled.div` diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index cc1f9dd529bce3..4863d6519de070 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -101,7 +101,7 @@ export function SpanFlyout({ const dbContext = span.span.db; const httpContext = span.span.http; const spanTypes = getSpanTypes(span); - const spanHttpStatusCode = httpContext?.response.status_code; + const spanHttpStatusCode = httpContext?.response?.status_code; const spanHttpUrl = httpContext?.url?.original; const spanHttpMethod = httpContext?.method; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx index 2020b8252035b7..df95577c81eff7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx @@ -27,8 +27,8 @@ import { DroppedSpansWarning } from './DroppedSpansWarning'; interface Props { onClose: () => void; transaction?: Transaction; - errorCount: number; - traceRootDuration?: number; + errorCount?: number; + rootTransactionDuration?: number; } function TransactionPropertiesTable({ @@ -49,8 +49,8 @@ function TransactionPropertiesTable({ export function TransactionFlyout({ transaction: transactionDoc, onClose, - errorCount, - traceRootDuration + errorCount = 0, + rootTransactionDuration }: Props) { if (!transactionDoc) { return null; @@ -84,7 +84,7 @@ export function TransactionFlyout({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx new file mode 100644 index 00000000000000..426088f0bb36a9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx @@ -0,0 +1,59 @@ +/* + * 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 { Location } from 'history'; +import React from 'react'; +import { SpanFlyout } from './SpanFlyout'; +import { TransactionFlyout } from './TransactionFlyout'; +import { IWaterfall } from './waterfall_helpers/waterfall_helpers'; + +interface Props { + waterfallItemId?: string; + waterfall: IWaterfall; + location: Location; + toggleFlyout: ({ location }: { location: Location }) => void; +} +export const WaterfallFlyout: React.FC = ({ + waterfallItemId, + waterfall, + location, + toggleFlyout +}) => { + const currentItem = waterfall.items.find(item => item.id === waterfallItemId); + + if (!currentItem) { + return null; + } + + switch (currentItem.docType) { + case 'span': + const parentTransaction = + currentItem.parent?.docType === 'transaction' + ? currentItem.parent?.doc + : undefined; + + return ( + toggleFlyout({ location })} + /> + ); + case 'transaction': + return ( + toggleFlyout({ location })} + rootTransactionDuration={ + waterfall.rootTransaction?.transaction.duration.us + } + errorCount={waterfall.errorsPerTransaction[currentItem.id]} + /> + ); + default: + return null; + } +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index 8d4fab4aa8dd91..8a82547d717db2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -13,12 +13,12 @@ import { i18n } from '@kbn/i18n'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; import { asDuration } from '../../../../../../utils/formatters'; -import { ErrorCountBadge } from '../../ErrorCountBadge'; +import { ErrorCount } from '../../ErrorCount'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; -type ItemType = 'transaction' | 'span'; +type ItemType = 'transaction' | 'span' | 'error'; interface IContainerStyleProps { type: ItemType; @@ -89,24 +89,29 @@ interface IWaterfallItemProps { } function PrefixIcon({ item }: { item: IWaterfallItem }) { - if (item.docType === 'span') { - // icon for database spans - const isDbType = item.span.span.type.startsWith('db'); - if (isDbType) { - return ; + switch (item.docType) { + case 'span': { + // icon for database spans + const isDbType = item.doc.span.type.startsWith('db'); + if (isDbType) { + return ; + } + + // omit icon for other spans + return null; } - - // omit icon for other spans - return null; - } - - // icon for RUM agent transactions - if (isRumAgentName(item.transaction.agent.name)) { - return ; + case 'transaction': { + // icon for RUM agent transactions + if (isRumAgentName(item.doc.agent.name)) { + return ; + } + + // icon for other transactions + return ; + } + default: + return null; } - - // icon for other transactions - return ; } interface SpanActionToolTipProps { @@ -117,11 +122,9 @@ const SpanActionToolTip: React.FC = ({ item, children }) => { - if (item && item.docType === 'span') { + if (item?.docType === 'span') { return ( - + <>{children} ); @@ -140,9 +143,8 @@ function Duration({ item }: { item: IWaterfallItem }) { function HttpStatusCode({ item }: { item: IWaterfallItem }) { // http status code for transactions of type 'request' const httpStatusCode = - item.docType === 'transaction' && - item.transaction.transaction.type === 'request' - ? item.transaction.transaction.result + item.docType === 'transaction' && item.doc.transaction.type === 'request' + ? item.doc.transaction.result : undefined; if (!httpStatusCode) { @@ -153,14 +155,18 @@ function HttpStatusCode({ item }: { item: IWaterfallItem }) { } function NameLabel({ item }: { item: IWaterfallItem }) { - if (item.docType === 'span') { - return {item.name}; + switch (item.docType) { + case 'span': + return {item.doc.span.name}; + case 'transaction': + return ( + +
{item.doc.transaction.name}
+
+ ); + default: + return null; } - return ( - -
{item.name}
-
- ); } export function WaterfallItem({ @@ -210,24 +216,17 @@ export function WaterfallItem({ {errorCount > 0 && item.docType === 'transaction' ? ( - { - event.stopPropagation(); - }} - onClickAriaLabel={tooltipContent} - > - {errorCount} - + ) : null} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index d53b4077d9759e..b48fc1cf7ca27e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -4,31 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { Component } from 'react'; +import React from 'react'; // @ts-ignore import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { IUrlParams } from '../../../../../../context/UrlParamsContext/types'; +import { px } from '../../../../../../style/variables'; +import { history } from '../../../../../../utils/history'; // @ts-ignore import Timeline from '../../../../../shared/charts/Timeline'; +import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; +import { getAgentMarks } from '../Marks/get_agent_marks'; +import { getErrorMarks } from '../Marks/get_error_marks'; +import { WaterfallFlyout } from './WaterfallFlyout'; +import { WaterfallItem } from './WaterfallItem'; import { - APMQueryParams, - fromQuery, - toQuery -} from '../../../../../shared/Links/url_helpers'; -import { history } from '../../../../../../utils/history'; -import { AgentMark } from '../get_agent_marks'; -import { SpanFlyout } from './SpanFlyout'; -import { TransactionFlyout } from './TransactionFlyout'; -import { - IServiceColors, IWaterfall, IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; -import { WaterfallItem } from './WaterfallItem'; const Container = styled.div` transition: 0.1s padding ease; @@ -43,138 +38,105 @@ const TIMELINE_MARGINS = { bottom: 0 }; +const toggleFlyout = ({ + item, + location +}: { + item?: IWaterfallItem; + location: Location; +}) => { + history.replace({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + flyoutDetailTab: undefined, + waterfallItemId: item?.id + }) + }); +}; + +const WaterfallItemsContainer = styled.div<{ + paddingTop: number; +}>` + padding-top: ${props => px(props.paddingTop)}; +`; + interface Props { - agentMarks: AgentMark[]; - urlParams: IUrlParams; + waterfallItemId?: string; waterfall: IWaterfall; location: Location; - serviceColors: IServiceColors; exceedsMax: boolean; } -export class Waterfall extends Component { - public onOpenFlyout = (item: IWaterfallItem) => { - this.setQueryParams({ - flyoutDetailTab: undefined, - waterfallItemId: String(item.id) - }); - }; +export const Waterfall: React.FC = ({ + waterfall, + exceedsMax, + waterfallItemId, + location +}) => { + 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; - public onCloseFlyout = () => { - this.setQueryParams({ - flyoutDetailTab: undefined, - waterfallItemId: undefined - }); - }; + const { serviceColors, duration } = waterfall; - public renderWaterfallItem = (item: IWaterfallItem) => { - const { serviceColors, waterfall, urlParams }: Props = this.props; + const agentMarks = getAgentMarks(waterfall.entryTransaction); + const errorMarks = getErrorMarks(waterfall.items, serviceColors); + + const renderWaterfallItem = (item: IWaterfallItem) => { + if (item.docType === 'error') { + return null; + } const errorCount = item.docType === 'transaction' - ? waterfall.errorCountByTransactionId[item.transaction.transaction.id] + ? waterfall.errorsPerTransaction[item.doc.transaction.id] : 0; return ( this.onOpenFlyout(item)} + onClick={() => toggleFlyout({ item, location })} /> ); }; - public getFlyOut = () => { - const { waterfall, urlParams } = this.props; - - const currentItem = - urlParams.waterfallItemId && - waterfall.itemsById[urlParams.waterfallItemId]; - - if (!currentItem) { - return null; - } - - switch (currentItem.docType) { - case 'span': - const parentTransaction = waterfall.getTransactionById( - currentItem.parentId - ); - - return ( - - ); - case 'transaction': - return ( - - ); - default: - return null; - } - }; - - public render() { - const { waterfall, exceedsMax } = this.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.orderedItems.length; - - return ( - - {exceedsMax ? ( - - ) : null} - - -
- {waterfall.orderedItems.map(this.renderWaterfallItem)} -
-
- - {this.getFlyOut()} -
- ); - } - - private setQueryParams(params: APMQueryParams) { - const { location } = this.props; - history.replace({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - ...params - }) - }); - } -} + return ( + + {exceedsMax && ( + + )} + + + + {waterfall.items.map(renderWaterfallItem)} + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index 6f61f621676386..ece396bc4cfc4f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -24,145 +24,44 @@ Object { "name": "GET /api", }, }, - "errorCountByTransactionId": Object { + "errorsCount": 1, + "errorsPerTransaction": Object { "myTransactionId1": 2, "myTransactionId2": 3, }, - "getTransactionById": [Function], - "itemsById": Object { - "mySpanIdA": Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 40498, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, + "items": Array [ + Object { + "doc": Object { "processor": Object { - "event": "span", + "event": "transaction", }, "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", + "name": "opbeans-node", }, "timestamp": Object { - "us": 1549324795824504, + "us": 1549324795784006, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795824504, - }, - "mySpanIdB": Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 41627, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { "duration": Object { - "us": 481, + "us": 49660, }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", + "id": "myTransactionId1", + "name": "GET /api", }, }, - "timestamp": 1549324795825633, - }, - "mySpanIdC": Object { - "childIds": Array [], - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 43899, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795827905, }, - "mySpanIdD": Object { - "childIds": Array [ - "myTransactionId2", - ], - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 1754, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { + Object { + "doc": Object { "parent": Object { "id": "myTransactionId1", }, @@ -189,59 +88,45 @@ Object { "id": "myTransactionId1", }, }, - "timestamp": 1549324795785760, - }, - "myTransactionId1": Object { - "childIds": Array [ - "mySpanIdD", - ], - "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", }, - "id": "myTransactionId1", - "name": "GET /api", }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, - }, - "myTransactionId2": Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - "offset": 39298, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", + "parentId": "myTransactionId1", "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdD", }, @@ -262,181 +147,403 @@ Object { "us": 8634, }, "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, "name": "Api::ProductsController#index", }, }, - }, - }, - "orderedItems": Array [ - Object { - "childIds": Array [ - "mySpanIdD", - ], "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, }, - "transaction": Object { - "duration": Object { - "us": 49660, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, "id": "myTransactionId1", - "name": "GET /api", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, + "parentId": "myTransactionId1", + "skew": 0, }, + "parentId": "mySpanIdD", + "skew": 0, }, Object { - "childIds": Array [ - "myTransactionId2", - ], - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 1754, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { + "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 6161, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, }, - "timestamp": 1549324795785760, + "parentId": "myTransactionId2", + "skew": 0, }, Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - "offset": 39298, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", - "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "mySpanIdA", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 481, + }, + "id": "mySpanIdB", + "name": "SELECT FROM products", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795825633, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "name": "Api::ProductsController#index", }, }, - }, - Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 40498, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, + "duration": 481, + "id": "mySpanIdB", + "offset": 41627, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", }, - "transaction": Object { + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, }, + "parentId": "myTransactionId2", + "skew": 0, }, - "timestamp": 1549324795824504, - }, - Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 41627, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -448,13 +555,13 @@ Object { }, "span": Object { "duration": Object { - "us": 481, + "us": 532, }, - "id": "mySpanIdB", - "name": "SELECT FROM products", + "id": "mySpanIdC", + "name": "SELECT FROM product", }, "timestamp": Object { - "us": 1549324795825633, + "us": 1549324795827905, }, "trace": Object { "id": "myTraceId", @@ -463,57 +570,223 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795825633, - }, - Object { - "childIds": Array [], "docType": "span", "duration": 532, "id": "mySpanIdC", - "name": "SELECT FROM product", "offset": 43899, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "agent": Object { + "name": "ruby", + "version": "2", + }, + "error": Object { + "grouping_key": "errorGroupingKey1", + "id": "error1", + "log": Object { + "message": "error message", + }, + }, "parent": Object { - "id": "mySpanIdA", + "id": "myTransactionId1", }, "processor": Object { - "event": "span", + "event": "error", }, "service": Object { "name": "opbeans-ruby", }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, "timestamp": Object { - "us": 1549324795827905, + "us": 1549324795810000, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId2", + "id": "myTransactionId1", + }, + }, + "docType": "error", + "duration": 0, + "id": "error1", + "offset": 25994, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, - "timestamp": 1549324795827905, + "parentId": "myTransactionId1", + "skew": 0, }, ], - "serviceColors": Object { - "opbeans-node": "#3185fc", - "opbeans-ruby": "#00b3a4", - }, - "services": Array [ - "opbeans-node", - "opbeans-ruby", - ], - "traceRoot": Object { + "rootTransaction": Object { "processor": Object { "event": "transaction", }, @@ -534,7 +807,10 @@ Object { "name": "GET /api", }, }, - "traceRootDuration": 49660, + "serviceColors": Object { + "opbeans-node": "#3185fc", + "opbeans-ruby": "#00b3a4", + }, } `; @@ -562,221 +838,24 @@ Object { "us": 8634, }, "id": "myTransactionId2", - "name": "Api::ProductsController#index", - }, - }, - "errorCountByTransactionId": Object { - "myTransactionId1": 2, - "myTransactionId2": 3, - }, - "getTransactionById": [Function], - "itemsById": Object { - "mySpanIdA": Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 1200, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795824504, - }, - "mySpanIdB": Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 2329, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 481, - }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795825633, - }, - "mySpanIdC": Object { - "childIds": Array [], - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 4601, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795827905, - }, - "mySpanIdD": Object { - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 0, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId1", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-node", - }, - "span": Object { - "duration": Object { - "us": 47557, - }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - }, - "timestamp": Object { - "us": 1549324795785760, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId1", - }, - }, - "timestamp": 1549324795785760, - }, - "myTransactionId1": Object { - "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, }, }, - }, - "myTransactionId2": Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", "name": "Api::ProductsController#index", - "offset": 0, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", - "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + }, + }, + "errorsCount": 0, + "errorsPerTransaction": Object { + "myTransactionId1": 2, + "myTransactionId2": 3, + }, + "items": Array [ + Object { + "doc": Object { "parent": Object { "id": "mySpanIdD", }, @@ -797,65 +876,26 @@ Object { "us": 8634, }, "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, "name": "Api::ProductsController#index", }, }, - }, - }, - "orderedItems": Array [ - Object { - "childIds": Array [ - "mySpanIdA", - ], "docType": "transaction", "duration": 8634, - "errorCount": 3, "id": "myTransactionId2", - "name": "Api::ProductsController#index", "offset": 0, + "parent": undefined, "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { - "parent": Object { - "id": "mySpanIdD", - }, - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "timestamp": Object { - "us": 1549324795823304, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 8634, - }, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - }, - }, }, Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 1200, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { + "doc": Object { "parent": Object { "id": "myTransactionId2", }, @@ -882,19 +922,55 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795824504, - }, - Object { - "childIds": Array [], "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 2329, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -921,19 +997,90 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795825633, - }, - Object { - "childIds": Array [], "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 4601, + "duration": 481, + "id": "mySpanIdB", + "offset": 2329, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -960,16 +1107,90 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795827905, + "docType": "span", + "duration": 532, + "id": "mySpanIdC", + "offset": 4601, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + "parentId": "mySpanIdA", + "skew": 0, }, ], - "serviceColors": Object { - "opbeans-ruby": "#3185fc", - }, - "services": Array [ - "opbeans-ruby", - ], - "traceRoot": Object { + "rootTransaction": Object { "processor": Object { "event": "transaction", }, @@ -990,30 +1211,61 @@ Object { "name": "GET /api", }, }, - "traceRootDuration": 49660, + "serviceColors": Object { + "opbeans-ruby": "#3185fc", + }, } `; exports[`waterfall_helpers getWaterfallItems should handle cyclic references 1`] = ` Array [ Object { - "childIds": Array [ - "a", - ], + "doc": Object { + "timestamp": Object { + "us": 10, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "transaction", "id": "a", "offset": 0, + "parent": undefined, "skew": 0, - "timestamp": 10, }, Object { - "childIds": Array [ - "a", - ], - "id": "a", + "doc": Object { + "parent": Object { + "id": "a", + }, + "span": Object { + "id": "b", + }, + "timestamp": Object { + "us": 20, + }, + }, + "docType": "span", + "id": "b", "offset": 10, + "parent": Object { + "doc": Object { + "timestamp": Object { + "us": 10, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "transaction", + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "skew": undefined, - "timestamp": 20, + "skew": 0, }, ] `; @@ -1021,89 +1273,280 @@ Array [ exports[`waterfall_helpers getWaterfallItems should order items correctly 1`] = ` Array [ Object { - "childIds": Array [ - "b2", - "b", - ], + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, "docType": "transaction", "duration": 9480, - "errorCount": 0, "id": "a", - "name": "APIRestController#products", "offset": 0, - "serviceName": "opbeans-java", + "parent": undefined, "skew": 0, - "timestamp": 1536763736366000, - "transaction": Object {}, }, Object { - "childIds": Array [], + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b2", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736367000, + }, + "transaction": Object { + "id": "a", + }, + }, "docType": "span", "duration": 4694, "id": "b2", - "name": "GET [0:0:0:0:0:0:0:1]", "offset": 1000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "serviceName": "opbeans-java", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, "transaction": Object { "id": "a", }, }, - "timestamp": 1536763736367000, - }, - Object { - "childIds": Array [ - "c", - ], "docType": "span", "duration": 4694, "id": "b", - "name": "GET [0:0:0:0:0:0:0:1]", "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "serviceName": "opbeans-java", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "parent": Object { + "id": "b", + }, + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736369000, + }, "transaction": Object { - "id": "a", + "id": "c", + "name": "APIRestController#productsRemote", }, }, - "timestamp": 1536763736368000, - }, - Object { - "childIds": Array [ - "d", - ], "docType": "transaction", "duration": 3581, - "errorCount": 0, "id": "c", - "name": "APIRestController#productsRemote", "offset": 3000, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "span", + "duration": 4694, + "id": "b", + "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, + "parentId": "a", + "skew": 0, + }, "parentId": "b", - "serviceName": "opbeans-java", "skew": 0, - "timestamp": 1536763736369000, - "transaction": Object {}, }, Object { - "childIds": Array [], + "doc": Object { + "parent": Object { + "id": "c", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "d", + "name": "SELECT", + }, + "timestamp": Object { + "us": 1536763736371000, + }, + "transaction": Object { + "id": "c", + }, + }, "docType": "span", "duration": 210, "id": "d", - "name": "SELECT", "offset": 5000, - "parentId": "c", - "serviceName": "opbeans-java", - "skew": 0, - "span": Object { - "transaction": Object { - "id": "c", + "parent": Object { + "doc": Object { + "parent": Object { + "id": "b", + }, + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736369000, + }, + "transaction": Object { + "id": "c", + "name": "APIRestController#productsRemote", + }, + }, + "docType": "transaction", + "duration": 3581, + "id": "c", + "offset": 3000, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "span", + "duration": 4694, + "id": "b", + "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, + "parentId": "a", + "skew": 0, }, + "parentId": "b", + "skew": 0, }, - "timestamp": 1536763736371000, + "parentId": "c", + "skew": 0, }, ] `; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index 6166515fd9d38d..426842bc02f510 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -11,8 +11,10 @@ import { getClockSkew, getOrderedWaterfallItems, getWaterfall, - IWaterfallItem + IWaterfallItem, + IWaterfallTransaction } from './waterfall_helpers'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; describe('waterfall_helpers', () => { describe('getWaterfall', () => { @@ -80,7 +82,7 @@ describe('waterfall_helpers', () => { }, timestamp: { us: 1549324795785760 } } as Span, - { + ({ parent: { id: 'mySpanIdD' }, processor: { event: 'transaction' }, trace: { id: 'myTraceId' }, @@ -88,10 +90,36 @@ describe('waterfall_helpers', () => { transaction: { duration: { us: 8634 }, name: 'Api::ProductsController#index', - id: 'myTransactionId2' + id: 'myTransactionId2', + marks: { + agent: { + domInteractive: 382, + domComplete: 383, + timeToFirstByte: 14 + } + } }, timestamp: { us: 1549324795823304 } - } as Transaction + } as unknown) as Transaction, + ({ + processor: { event: 'error' }, + parent: { id: 'myTransactionId1' }, + timestamp: { us: 1549324795810000 }, + trace: { id: 'myTraceId' }, + transaction: { id: 'myTransactionId1' }, + error: { + id: 'error1', + grouping_key: 'errorGroupingKey1', + log: { + message: 'error message' + } + }, + service: { name: 'opbeans-ruby' }, + agent: { + name: 'ruby', + version: '2' + } + } as unknown) as APMError ]; it('should return full waterfall', () => { @@ -107,8 +135,10 @@ describe('waterfall_helpers', () => { }, entryTransactionId ); - expect(waterfall.orderedItems.length).toBe(6); - expect(waterfall.orderedItems[0].id).toBe('myTransactionId1'); + + expect(waterfall.items.length).toBe(7); + expect(waterfall.items[0].id).toBe('myTransactionId1'); + expect(waterfall.errorsCount).toEqual(1); expect(waterfall).toMatchSnapshot(); }); @@ -125,26 +155,11 @@ describe('waterfall_helpers', () => { }, entryTransactionId ); - expect(waterfall.orderedItems.length).toBe(4); - expect(waterfall.orderedItems[0].id).toBe('myTransactionId2'); - expect(waterfall).toMatchSnapshot(); - }); - it('getTransactionById', () => { - const entryTransactionId = 'myTransactionId1'; - const errorsPerTransaction = { - myTransactionId1: 2, - myTransactionId2: 3 - }; - const waterfall = getWaterfall( - { - trace: { items: hits, exceedsMax: false }, - errorsPerTransaction - }, - entryTransactionId - ); - const transaction = waterfall.getTransactionById('myTransactionId2'); - expect(transaction!.transaction.id).toBe('myTransactionId2'); + expect(waterfall.items.length).toBe(4); + expect(waterfall.items[0].id).toBe('myTransactionId2'); + expect(waterfall.errorsCount).toEqual(0); + expect(waterfall).toMatchSnapshot(); }); }); @@ -152,84 +167,102 @@ describe('waterfall_helpers', () => { it('should order items correctly', () => { const items: IWaterfallItem[] = [ { + docType: 'span', + doc: { + parent: { id: 'c' }, + service: { name: 'opbeans-java' }, + transaction: { + id: 'c' + }, + timestamp: { us: 1536763736371000 }, + span: { + id: 'd', + name: 'SELECT' + } + } as Span, id: 'd', parentId: 'c', - serviceName: 'opbeans-java', - name: 'SELECT', duration: 210, - timestamp: 1536763736371000, offset: 0, - skew: 0, + skew: 0 + }, + { docType: 'span', - span: { + doc: { + parent: { id: 'a' }, + service: { name: 'opbeans-java' }, transaction: { - id: 'c' + id: 'a' + }, + timestamp: { us: 1536763736368000 }, + span: { + id: 'b', + name: 'GET [0:0:0:0:0:0:0:1]' } - } as Span - }, - { + } as Span, id: 'b', parentId: 'a', - serviceName: 'opbeans-java', - name: 'GET [0:0:0:0:0:0:0:1]', duration: 4694, - timestamp: 1536763736368000, offset: 0, - skew: 0, + skew: 0 + }, + { docType: 'span', - span: { + doc: { + parent: { id: 'a' }, + service: { name: 'opbeans-java' }, transaction: { id: 'a' + }, + timestamp: { us: 1536763736367000 }, + span: { + id: 'b2', + name: 'GET [0:0:0:0:0:0:0:1]' } - } as Span - }, - { + } as Span, id: 'b2', parentId: 'a', - serviceName: 'opbeans-java', - name: 'GET [0:0:0:0:0:0:0:1]', duration: 4694, - timestamp: 1536763736367000, offset: 0, - skew: 0, - docType: 'span', - span: { - transaction: { - id: 'a' - } - } as Span + skew: 0 }, { + docType: 'transaction', + doc: { + parent: { id: 'b' }, + service: { name: 'opbeans-java' }, + timestamp: { us: 1536763736369000 }, + transaction: { id: 'c', name: 'APIRestController#productsRemote' } + } as Transaction, id: 'c', parentId: 'b', - serviceName: 'opbeans-java', - name: 'APIRestController#productsRemote', duration: 3581, - timestamp: 1536763736369000, offset: 0, - skew: 0, - docType: 'transaction', - transaction: {} as Transaction, - errorCount: 0 + skew: 0 }, { + docType: 'transaction', + doc: { + service: { name: 'opbeans-java' }, + timestamp: { us: 1536763736366000 }, + transaction: { + id: 'a', + name: 'APIRestController#products' + } + } as Transaction, id: 'a', - serviceName: 'opbeans-java', - name: 'APIRestController#products', duration: 9480, - timestamp: 1536763736366000, offset: 0, - skew: 0, - docType: 'transaction', - transaction: {} as Transaction, - errorCount: 0 + skew: 0 } ]; const childrenByParentId = groupBy(items, hit => hit.parentId ? hit.parentId : 'root' ); - const entryTransactionItem = childrenByParentId.root[0]; + const entryTransactionItem = childrenByParentId + .root[0] as IWaterfallTransaction; + expect( getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) ).toMatchSnapshot(); @@ -237,13 +270,32 @@ describe('waterfall_helpers', () => { it('should handle cyclic references', () => { const items = [ - { id: 'a', timestamp: 10 } as IWaterfallItem, - { id: 'a', parentId: 'a', timestamp: 20 } as IWaterfallItem + { + docType: 'transaction', + id: 'a', + doc: ({ + transaction: { id: 'a' }, + timestamp: { us: 10 } + } as unknown) as Transaction + } as IWaterfallItem, + { + docType: 'span', + id: 'b', + parentId: 'a', + doc: ({ + span: { + id: 'b' + }, + parent: { id: 'a' }, + timestamp: { us: 20 } + } as unknown) as Span + } as IWaterfallItem ]; const childrenByParentId = groupBy(items, hit => hit.parentId ? hit.parentId : 'root' ); - const entryTransactionItem = childrenByParentId.root[0]; + const entryTransactionItem = childrenByParentId + .root[0] as IWaterfallTransaction; expect( getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) ).toMatchSnapshot(); @@ -254,12 +306,17 @@ describe('waterfall_helpers', () => { it('should adjust when child starts before parent', () => { const child = { docType: 'transaction', - timestamp: 0, + doc: { + timestamp: { us: 0 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -270,12 +327,17 @@ describe('waterfall_helpers', () => { it('should not adjust when child starts after parent has ended', () => { const child = { docType: 'transaction', - timestamp: 250, + doc: { + timestamp: { us: 250 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -286,12 +348,17 @@ describe('waterfall_helpers', () => { it('should not adjust when child starts within parent duration', () => { const child = { docType: 'transaction', - timestamp: 150, + doc: { + timestamp: { us: 150 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -305,7 +372,27 @@ describe('waterfall_helpers', () => { } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'span', + doc: { + timestamp: { us: 100 } + }, + duration: 100, + skew: 5 + } as IWaterfallItem; + + expect(getClockSkew(child, parent)).toBe(5); + }); + + it('should return parent skew for errors', () => { + const child = { + docType: 'error' + } as IWaterfallItem; + + const parent = { + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 2a69c5f51173d5..1af6cddb3ba4a7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -6,60 +6,52 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { + first, flatten, groupBy, - indexBy, + isEmpty, sortBy, + sum, uniq, - zipObject, - isEmpty, - first + zipObject } from 'lodash'; import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; import { Span } from '../../../../../../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction'; -interface IWaterfallIndex { - [key: string]: IWaterfallItem | undefined; -} - interface IWaterfallGroup { [key: string]: IWaterfallItem[]; } export interface IWaterfall { entryTransaction?: Transaction; - traceRoot?: Transaction; - traceRootDuration?: number; + rootTransaction?: Transaction; /** * Duration in us */ duration: number; - services: string[]; - orderedItems: IWaterfallItem[]; - itemsById: IWaterfallIndex; - getTransactionById: (id?: IWaterfallItem['id']) => Transaction | undefined; - errorCountByTransactionId: TraceAPIResponse['errorsPerTransaction']; + items: IWaterfallItem[]; + errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; + errorsCount: number; serviceColors: IServiceColors; } -interface IWaterfallItemBase { - id: string | number; +interface IWaterfallItemBase { + docType: U; + doc: T; + + id: string; + + parent?: IWaterfallItem; parentId?: string; - serviceName: string; - name: string; /** * Duration in us */ duration: number; - /** - * start timestamp in us - */ - timestamp: number; - /** * offset from first item in us */ @@ -69,53 +61,53 @@ interface IWaterfallItemBase { * skew from timestamp in us */ skew: number; - childIds?: Array; -} - -interface IWaterfallItemTransaction extends IWaterfallItemBase { - transaction: Transaction; - docType: 'transaction'; - errorCount: number; } -interface IWaterfallItemSpan extends IWaterfallItemBase { - span: Span; - docType: 'span'; -} +export type IWaterfallTransaction = IWaterfallItemBase< + Transaction, + 'transaction' +>; +export type IWaterfallSpan = IWaterfallItemBase; +export type IWaterfallError = IWaterfallItemBase; -export type IWaterfallItem = IWaterfallItemSpan | IWaterfallItemTransaction; +export type IWaterfallItem = + | IWaterfallTransaction + | IWaterfallSpan + | IWaterfallError; -function getTransactionItem( - transaction: Transaction, - errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'] -): IWaterfallItemTransaction { +function getTransactionItem(transaction: Transaction): IWaterfallTransaction { return { + docType: 'transaction', + doc: transaction, id: transaction.transaction.id, - parentId: transaction.parent && transaction.parent.id, - serviceName: transaction.service.name, - name: transaction.transaction.name, + parentId: transaction.parent?.id, duration: transaction.transaction.duration.us, - timestamp: transaction.timestamp.us, offset: 0, - skew: 0, - docType: 'transaction', - transaction, - errorCount: errorsPerTransaction[transaction.transaction.id] || 0 + skew: 0 }; } -function getSpanItem(span: Span): IWaterfallItemSpan { +function getSpanItem(span: Span): IWaterfallSpan { return { + docType: 'span', + doc: span, id: span.span.id, - parentId: span.parent && span.parent.id, - serviceName: span.service.name, - name: span.span.name, + parentId: span.parent?.id, duration: span.span.duration.us, - timestamp: span.timestamp.us, + offset: 0, + skew: 0 + }; +} + +function getErrorItem(error: APMError): IWaterfallError { + return { + docType: 'error', + doc: error, + id: error.error.id, + parentId: error.parent?.id, offset: 0, skew: 0, - docType: 'span', - span + duration: 0 }; } @@ -126,18 +118,17 @@ export function getClockSkew( if (!parentItem) { return 0; } - switch (item.docType) { - // don't calculate skew for spans. Just use parent's skew + // don't calculate skew for spans and errors. Just use parent's skew + case 'error': case 'span': return parentItem.skew; - // transaction is the inital entry in a service. Calculate skew for this, and it will be propogated to all child spans case 'transaction': { - const parentStart = parentItem.timestamp + parentItem.skew; + const parentStart = parentItem.doc.timestamp.us + parentItem.skew; // determine if child starts before the parent - const offsetStart = parentStart - item.timestamp; + const offsetStart = parentStart - item.doc.timestamp.us; if (offsetStart > 0) { const latency = Math.max(parentItem.duration - item.duration, 0) / 2; return offsetStart + latency; @@ -151,9 +142,14 @@ export function getClockSkew( export function getOrderedWaterfallItems( childrenByParentId: IWaterfallGroup, - entryTransactionItem: IWaterfallItem + entryWaterfallTransaction?: IWaterfallTransaction ) { + if (!entryWaterfallTransaction) { + return []; + } + const entryTimestamp = entryWaterfallTransaction.doc.timestamp.us; const visitedWaterfallItemSet = new Set(); + function getSortedChildren( item: IWaterfallItem, parentItem?: IWaterfallItem @@ -162,10 +158,16 @@ export function getOrderedWaterfallItems( return []; } visitedWaterfallItemSet.add(item); - const children = sortBy(childrenByParentId[item.id] || [], 'timestamp'); - item.childIds = children.map(child => child.id); - item.offset = item.timestamp - entryTransactionItem.timestamp; + const children = sortBy( + childrenByParentId[item.id] || [], + 'doc.timestamp.us' + ); + + item.parent = parentItem; + // get offset from the beginning of trace + item.offset = item.doc.timestamp.us - entryTimestamp; + // move the item to the right if it starts before its parent item.skew = getClockSkew(item, parentItem); const deepChildren = flatten( @@ -174,24 +176,21 @@ export function getOrderedWaterfallItems( return [item, ...deepChildren]; } - return getSortedChildren(entryTransactionItem); + return getSortedChildren(entryWaterfallTransaction); } -function getTraceRoot(childrenByParentId: IWaterfallGroup) { +function getRootTransaction(childrenByParentId: IWaterfallGroup) { const item = first(childrenByParentId.root); if (item && item.docType === 'transaction') { - return item.transaction; + return item.doc; } } -function getServices(items: IWaterfallItem[]) { - const serviceNames = items.map(item => item.serviceName); - return uniq(serviceNames); -} - export type IServiceColors = Record; -function getServiceColors(services: string[]) { +function getServiceColors(waterfallItems: IWaterfallItem[]) { + const services = uniq(waterfallItems.map(item => item.doc.service.name)); + const assignedColors = [ theme.euiColorVis1, theme.euiColorVis0, @@ -205,30 +204,35 @@ function getServiceColors(services: string[]) { return zipObject(services, assignedColors) as IServiceColors; } -function getDuration(items: IWaterfallItem[]) { - if (items.length === 0) { - return 0; - } - const timestampStart = items[0].timestamp; - const timestampEnd = Math.max( - ...items.map(item => item.timestamp + item.duration + item.skew) +const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) => + Math.max( + ...waterfallItems.map(item => item.offset + item.skew + item.duration), + 0 ); - return timestampEnd - timestampStart; -} -function createGetTransactionById(itemsById: IWaterfallIndex) { - return (id?: IWaterfallItem['id']) => { - if (!id) { - return undefined; +const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => + items.map(item => { + const docType = item.processor.event; + switch (docType) { + case 'span': + return getSpanItem(item as Span); + case 'transaction': + return getTransactionItem(item as Transaction); + case 'error': + return getErrorItem(item as APMError); } + }); - const item = itemsById[id]; - const isTransaction = item?.docType === 'transaction'; - if (isTransaction) { - return (item as IWaterfallItemTransaction).transaction; - } - }; -} +const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => + groupBy(waterfallItems, item => (item.parentId ? item.parentId : 'root')); + +const getEntryWaterfallTransaction = ( + entryTransactionId: string, + waterfallItems: IWaterfallItem[] +): IWaterfallTransaction | undefined => + waterfallItems.find( + item => item.docType === 'transaction' && item.id === entryTransactionId + ) as IWaterfallTransaction; export function getWaterfall( { trace, errorsPerTransaction }: TraceAPIResponse, @@ -236,59 +240,41 @@ export function getWaterfall( ): IWaterfall { if (isEmpty(trace.items) || !entryTransactionId) { return { - services: [], duration: 0, - orderedItems: [], - itemsById: {}, - getTransactionById: () => undefined, - errorCountByTransactionId: errorsPerTransaction, + items: [], + errorsPerTransaction, + errorsCount: sum(Object.values(errorsPerTransaction)), serviceColors: {} }; } - const waterfallItems = trace.items.map(traceItem => { - const docType = traceItem.processor.event; - switch (docType) { - case 'span': - return getSpanItem(traceItem as Span); - case 'transaction': - return getTransactionItem( - traceItem as Transaction, - errorsPerTransaction - ); - } - }); + const waterfallItems: IWaterfallItem[] = getWaterfallItems(trace.items); + + const childrenByParentId = getChildrenGroupedByParentId(waterfallItems); - const childrenByParentId = groupBy(waterfallItems, item => - item.parentId ? item.parentId : 'root' + const entryWaterfallTransaction = getEntryWaterfallTransaction( + entryTransactionId, + waterfallItems ); - const entryTransactionItem = waterfallItems.find( - waterfallItem => - waterfallItem.docType === 'transaction' && - waterfallItem.id === entryTransactionId + + const items = getOrderedWaterfallItems( + childrenByParentId, + entryWaterfallTransaction ); - const itemsById: IWaterfallIndex = indexBy(waterfallItems, 'id'); - const orderedItems = entryTransactionItem - ? getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) - : []; - const traceRoot = getTraceRoot(childrenByParentId); - const duration = getDuration(orderedItems); - const traceRootDuration = traceRoot && traceRoot.transaction.duration.us; - const services = getServices(orderedItems); - const getTransactionById = createGetTransactionById(itemsById); - const serviceColors = getServiceColors(services); - const entryTransaction = getTransactionById(entryTransactionId); + + const rootTransaction = getRootTransaction(childrenByParentId); + const duration = getWaterfallDuration(items); + const serviceColors = getServiceColors(items); + + const entryTransaction = entryWaterfallTransaction?.doc; return { entryTransaction, - traceRoot, - traceRootDuration, + rootTransaction, duration, - services, - orderedItems, - itemsById, - getTransactionById, - errorCountByTransactionId: errorsPerTransaction, + items, + errorsPerTransaction, + errorsCount: items.filter(item => item.docType === 'error').length, serviceColors }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts deleted file mode 100644 index af76451db68b7b..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { sortBy } from 'lodash'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; - -export interface AgentMark { - name: string; - us: number; -} - -export function getAgentMarks(transaction: Transaction): AgentMark[] { - if (!(transaction.transaction.marks && transaction.transaction.marks.agent)) { - return []; - } - - return sortBy( - Object.entries(transaction.transaction.marks.agent).map(([name, ms]) => ({ - name, - us: ms * 1000 - })), - 'us' - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx index 2f34cc86c5cfc2..77be5c999f7c36 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx @@ -6,16 +6,13 @@ import { Location } from 'history'; import React from 'react'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { getAgentMarks } from './get_agent_marks'; import { ServiceLegends } from './ServiceLegends'; import { Waterfall } from './Waterfall'; import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; interface Props { urlParams: IUrlParams; - transaction: Transaction; location: Location; waterfall: IWaterfall; exceedsMax: boolean; @@ -24,11 +21,9 @@ interface Props { export function WaterfallContainer({ location, urlParams, - transaction, waterfall, exceedsMax }: Props) { - const agentMarks = getAgentMarks(transaction); if (!waterfall) { return null; } @@ -37,10 +32,8 @@ export function WaterfallContainer({
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx new file mode 100644 index 00000000000000..62b5f7834d3a9d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.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 { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { expectTextsInDocument } from '../../../../../utils/testHelpers'; +import { ErrorCount } from '../ErrorCount'; + +describe('ErrorCount', () => { + it('shows singular error message', () => { + const component = render(); + expectTextsInDocument(component, ['1 Error']); + }); + it('shows plural error message', () => { + const component = render(); + expectTextsInDocument(component, ['2 Errors']); + }); + it('prevents click propagation', () => { + const mock = jest.fn(); + const { getByText } = render( + + ); + fireEvent( + getByText('1 Error'), + new MouseEvent('click', { + bubbles: true, + cancelable: true + }) + ); + expect(mock).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index b56370a59c8e22..6dcab6c6b97c16 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -5,30 +5,29 @@ */ import { + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiPagination, EuiPanel, EuiSpacer, - EuiEmptyPrompt, - EuiTitle, - EuiPagination + EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { useState, useEffect } from 'react'; -import { sum } from 'lodash'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; -import { TransactionTabs } from './TransactionTabs'; -import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; -import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { px, units } from '../../../../style/variables'; import { history } from '../../../../utils/history'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; +import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; import { MaybeViewTraceLink } from './MaybeViewTraceLink'; -import { units, px } from '../../../../style/variables'; +import { TransactionTabs } from './TransactionTabs'; +import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; const PaginationContainer = styled.div` margin-left: ${px(units.quarter)}; @@ -140,8 +139,8 @@ export const WaterfallWithSummmary: React.FC = ({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 0312e94d7ee192..eba59f6e3ce44f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -18,7 +18,7 @@ interface Props extends EuiLinkAnchorProps { children?: React.ReactNode; } -export type APMLinkExtendProps = Omit; +export type APMLinkExtendProps = Omit; export const PERSISTENT_APM_PARAMS = [ 'kuery', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx similarity index 51% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 964debbedb2e42..7558f002c0afc4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -6,28 +6,25 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { EuiBadge } from '@elastic/eui'; +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import { px } from '../../../../public/style/variables'; -import { ErrorCountBadge } from '../../app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge'; import { units } from '../../../style/variables'; interface Props { count: number; } -const Badge = styled(ErrorCountBadge)` +const Badge = styled(EuiBadge)` margin-top: ${px(units.eighth)}; `; -const ErrorCountSummaryItem = ({ count }: Props) => { - return ( - - {i18n.translate('xpack.apm.transactionDetails.errorCount', { - defaultMessage: - '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', - values: { errorCount: count } - })} - - ); -}; - -export { ErrorCountSummaryItem }; +export const ErrorCountSummaryItemBadge = ({ count }: Props) => ( + + {i18n.translate('xpack.apm.transactionDetails.errorCount', { + defaultMessage: + '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', + values: { errorCount: count } + })} + +); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index 8b7380a18edc39..51da61cd7c1a6e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -8,7 +8,7 @@ import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { Summary } from './'; import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; -import { ErrorCountSummaryItem } from './ErrorCountSummaryItem'; +import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; import { isRumAgentName } from '../../../../common/agent_name'; import { HttpInfoSummaryItem } from './HttpInfoSummaryItem'; import { TransactionResultSummaryItem } from './TransactionResultSummaryItem'; @@ -54,7 +54,7 @@ const TransactionSummary = ({ parentType="trace" />, getTransactionResultSummaryItem(transaction), - errorCount ? : null, + errorCount ? : null, transaction.user_agent ? ( ) : null diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx new file mode 100644 index 00000000000000..33f5752b6389b0 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx @@ -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 React from 'react'; +import { ErrorCountSummaryItemBadge } from '../ErrorCountSummaryItemBadge'; +import { render } from '@testing-library/react'; +import { expectTextsInDocument } from '../../../../utils/testHelpers'; + +describe('ErrorCountSummaryItemBadge', () => { + it('shows singular error message', () => { + const component = render(); + expectTextsInDocument(component, ['1 Error']); + }); + it('shows plural error message', () => { + const component = render(); + expectTextsInDocument(component, ['2 Errors']); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index 848c975942ff64..99eb17386f847b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; -import Legend from '../Legend'; +import { Legend } from '../Legend'; import { unit, units, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index c46cbbbcccc0b8..557751a0f02268 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -2725,11 +2725,14 @@ Array [ @@ -2763,11 +2766,14 @@ Array [ @@ -2794,11 +2800,14 @@ Array [ @@ -5167,11 +5176,14 @@ Array [ Avg. @@ -5210,11 +5222,14 @@ Array [ 95th @@ -5253,11 +5268,14 @@ Array [ 99th @@ -5886,11 +5904,14 @@ Array [ @@ -5924,11 +5945,14 @@ Array [ @@ -5955,11 +5979,14 @@ Array [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js deleted file mode 100644 index 601482430b00f5..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js +++ /dev/null @@ -1,56 +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, { PureComponent } from 'react'; -import styled from 'styled-components'; -import { units, px, fontSizes } from '../../../../style/variables'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -const Container = styled.div` - display: flex; - align-items: center; - font-size: ${props => props.fontSize}; - color: ${theme.euiColorDarkShade}; - cursor: ${props => (props.clickable ? 'pointer' : 'initial')}; - opacity: ${props => (props.disabled ? 0.4 : 1)}; - user-select: none; -`; - -export const Indicator = styled.span` - width: ${props => px(props.radius)}; - height: ${props => px(props.radius)}; - margin-right: ${props => px(props.radius / 2)}; - background: ${props => props.color}; - border-radius: 100%; -`; - -export default class Legend extends PureComponent { - render() { - const { - onClick, - text, - color = theme.euiColorVis1, - fontSize = fontSizes.small, - radius = units.minus - 1, - disabled = false, - clickable = false, - indicator, - ...rest - } = this.props; - return ( - - {indicator ? indicator() : } - {text} - - ); - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx new file mode 100644 index 00000000000000..436b020bc9eba7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -0,0 +1,94 @@ +/* + * 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 theme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import styled from 'styled-components'; +import { fontSizes, px, units } from '../../../../style/variables'; + +export enum Shape { + circle = 'circle', + square = 'square' +} + +interface ContainerProps { + onClick: (e: Event) => void; + fontSize?: string; + clickable: boolean; + disabled: boolean; +} +const Container = styled.div` + display: flex; + align-items: center; + font-size: ${props => props.fontSize}; + color: ${theme.euiColorDarkShade}; + cursor: ${props => (props.clickable ? 'pointer' : 'initial')}; + opacity: ${props => (props.disabled ? 0.4 : 1)}; + user-select: none; +`; + +interface IndicatorProps { + radius: number; + color: string; + shape: Shape; + withMargin: boolean; +} +export const Indicator = styled.span` + width: ${props => px(props.radius)}; + height: ${props => px(props.radius)}; + margin-right: ${props => (props.withMargin ? px(props.radius / 2) : 0)}; + background: ${props => props.color}; + border-radius: ${props => { + return props.shape === Shape.circle ? '100%' : '0'; + }}; +`; + +interface Props { + onClick?: any; + text?: string; + color?: string; + fontSize?: string; + radius?: number; + disabled?: boolean; + clickable?: boolean; + shape?: Shape; + indicator?: () => React.ReactNode; +} + +export const Legend: React.FC = ({ + onClick, + text, + color = theme.euiColorVis1, + fontSize = fontSizes.small, + radius = units.minus - 1, + disabled = false, + clickable = false, + shape = Shape.circle, + indicator, + ...rest +}) => { + return ( + + {indicator ? ( + indicator() + ) : ( + + )} + {text} + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx similarity index 56% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js rename to x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index 8ee23d61fe0eb6..ffdbfe6cce7ec8 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import PropTypes from 'prop-types'; import { EuiToolTip } from '@elastic/eui'; -import Legend from '../Legend'; -import { units, px } from '../../../../style/variables'; -import styled from 'styled-components'; -import { asDuration } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import styled from 'styled-components'; +import { px, units } from '../../../../../style/variables'; +import { asDuration } from '../../../../../utils/formatters'; +import { Legend } from '../../Legend'; +import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; const NameContainer = styled.div` border-bottom: 1px solid ${theme.euiColorMediumShade}; @@ -23,33 +23,25 @@ const TimeContainer = styled.div` padding-top: ${px(units.half)}; `; -export default function AgentMarker({ agentMark, x }) { - const legendWidth = 11; +interface Props { + mark: AgentMark; +} + +export const AgentMarker: React.FC = ({ mark }) => { return ( -
+ <> - {agentMark.name} - {asDuration(agentMark.us)} + {mark.id} + {asDuration(mark.offset)}
} > -
+ ); -} - -AgentMarker.propTypes = { - agentMark: PropTypes.object.isRequired, - x: PropTypes.number.isRequired }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx new file mode 100644 index 00000000000000..51368a4fb946d5 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -0,0 +1,103 @@ +/* + * 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 { EuiPopover, EuiText } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + TRACE_ID, + TRANSACTION_ID +} from '../../../../../../common/elasticsearch_fieldnames'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { px, unit, units } from '../../../../../style/variables'; +import { asDuration } from '../../../../../utils/formatters'; +import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; +import { Legend, Shape } from '../../Legend'; + +interface Props { + mark: ErrorMark; +} + +const Popover = styled.div` + max-width: ${px(280)}; +`; + +const TimeLegend = styled(Legend)` + margin-bottom: ${px(unit)}; +`; + +const ErrorLink = styled(ErrorDetailLink)` + display: block; + margin: ${px(units.half)} 0 ${px(units.half)} 0; +`; + +const Button = styled(Legend)` + height: 20px; + display: flex; + align-items: flex-end; +`; + +export const ErrorMarker: React.FC = ({ mark }) => { + const { urlParams } = useUrlParams(); + const [isPopoverOpen, showPopover] = useState(false); + + const togglePopover = () => showPopover(!isPopoverOpen); + + const button = ( +