From 41302211236ee9871bdbe78536776911d113ba07 Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Thu, 21 Oct 2021 18:57:16 -0500 Subject: [PATCH 01/41] [Controls] Data view and field pickers (#116018) Added file picker and field picker to presentationUtil --- src/plugins/presentation_util/kibana.json | 2 +- .../data_view_picker.stories.tsx | 94 +++++++++++ .../data_view_picker/data_view_picker.tsx | 114 +++++++++++++ .../components/field_picker/field_picker.scss | 15 ++ .../field_picker/field_picker.stories.tsx | 89 ++++++++++ .../components/field_picker/field_picker.tsx | 152 ++++++++++++++++++ .../components/field_picker/field_search.tsx | 125 ++++++++++++++ 7 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_picker.scss create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_picker.tsx create mode 100644 src/plugins/presentation_util/public/components/field_picker/field_search.tsx diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index d7fe9b558e606..71ac224d1976a 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -10,6 +10,6 @@ "server": true, "ui": true, "extraPublicDirs": ["common/lib"], - "requiredPlugins": ["savedObjects"], + "requiredPlugins": ["savedObjects", "kibanaReact"], "optionalPlugins": [] } diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx new file mode 100644 index 0000000000000..1a29d0536a290 --- /dev/null +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; + +import { DataView, DataViewField, IIndexPatternFieldList } from '../../../../data_views/common'; + +import { StorybookParams } from '../../services/storybook'; +import { DataViewPicker } from './data_view_picker'; + +// TODO: we probably should remove this once the PR is merged that has better data views for stories +const flightFieldNames: string[] = [ + 'AvgTicketPrice', + 'Cancelled', + 'Carrier', + 'dayOfWeek', + 'Dest', + 'DestAirportID', + 'DestCityName', + 'DestCountry', + 'DestLocation', + 'DestRegion', + 'DestWeather', + 'DistanceKilometers', + 'DistanceMiles', + 'FlightDelay', + 'FlightDelayMin', + 'FlightDelayType', + 'FlightNum', + 'FlightTimeHour', + 'FlightTimeMin', + 'Origin', + 'OriginAirportID', + 'OriginCityName', + 'OriginCountry', + 'OriginLocation', + 'OriginRegion', + 'OriginWeather', + 'timestamp', +]; +const flightFieldByName: { [key: string]: DataViewField } = {}; +flightFieldNames.forEach( + (flightFieldName) => + (flightFieldByName[flightFieldName] = { + name: flightFieldName, + type: 'string', + } as unknown as DataViewField) +); + +// Change some types manually for now +flightFieldByName.Cancelled = { name: 'Cancelled', type: 'boolean' } as DataViewField; +flightFieldByName.timestamp = { name: 'timestamp', type: 'date' } as DataViewField; + +const flightFields: DataViewField[] = Object.values(flightFieldByName); +const storybookFlightsDataView: DataView = { + id: 'demoDataFlights', + title: 'demo data flights', + fields: flightFields as unknown as IIndexPatternFieldList, + getFieldByName: (name: string) => flightFieldByName[name], +} as unknown as DataView; + +export default { + component: DataViewPicker, + title: 'Data View Picker', + argTypes: {}, +}; + +export function Example({}: {} & StorybookParams) { + const dataViews = [storybookFlightsDataView]; + + const [dataView, setDataView] = useState(undefined); + + const onChange = (newId: string) => { + const newIndexPattern = dataViews.find((ip) => ip.id === newId); + + setDataView(newIndexPattern); + }; + + const triggerLabel = dataView?.title || 'Choose Data View'; + + return ( + + ); +} diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx new file mode 100644 index 0000000000000..38ec4f16f9432 --- /dev/null +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { EuiPopover, EuiPopoverTitle, EuiSelectable, EuiSelectableProps } from '@elastic/eui'; +import { DataView } from '../../../../data_views/common'; + +import { ToolbarButton, ToolbarButtonProps } from '../../../../kibana_react/public'; + +export type DataViewTriggerProps = ToolbarButtonProps & { + label: string; + title?: string; +}; + +export function DataViewPicker({ + dataViews, + selectedDataViewId, + onChangeIndexPattern, + trigger, + selectableProps, +}: { + dataViews: DataView[]; + selectedDataViewId?: string; + trigger: DataViewTriggerProps; + onChangeIndexPattern: (newId: string) => void; + selectableProps?: EuiSelectableProps; +}) { + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + + const isMissingCurrent = !dataViews.some(({ id }) => id === selectedDataViewId); + + // be careful to only add color with a value, otherwise it will fallbacks to "primary" + const colorProp = isMissingCurrent + ? { + color: 'danger' as const, + } + : {}; + + const createTrigger = function () { + const { label, title, ...rest } = trigger; + return ( + setPopoverIsOpen(!isPopoverOpen)} + fullWidth + {...colorProp} + {...rest} + > + {label} + + ); + }; + + return ( + <> + setPopoverIsOpen(false)} + display="block" + panelPaddingSize="s" + ownFocus + > +
+ + {i18n.translate('presentationUtil.dataViewPicker.changeDataViewTitle', { + defaultMessage: 'Data view', + })} + + + {...selectableProps} + searchable + singleSelection="always" + options={dataViews.map(({ title, id }) => ({ + key: id, + label: title, + value: id, + checked: id === selectedDataViewId ? 'on' : undefined, + }))} + onChange={(choices) => { + const choice = choices.find(({ checked }) => checked) as unknown as { + value: string; + }; + onChangeIndexPattern(choice.value); + setPopoverIsOpen(false); + }} + searchProps={{ + compressed: true, + ...(selectableProps ? selectableProps.searchProps : undefined), + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + +
+
+ + ); +} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.scss b/src/plugins/presentation_util/public/components/field_picker/field_picker.scss new file mode 100644 index 0000000000000..c07cf99ed03d6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.scss @@ -0,0 +1,15 @@ +.presFieldPicker__fieldButton { + box-shadow: 0 .8px .8px rgba(0,0,0,.04),0 2.3px 2px rgba(0,0,0,.03); + background: #FFF; + border: 1px dashed transparent; +} + +.presFieldPicker__fieldPanel { + height: 300px; + overflow-y: scroll; +} + +.presFieldPicker__container--disabled { + opacity: .7; + pointer-events: none; +} \ No newline at end of file diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx new file mode 100644 index 0000000000000..c5654254ea70a --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.stories.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { FieldPicker } from './field_picker'; + +import { DataView, DataViewField, IIndexPatternFieldList } from '../../../../data_views/common'; + +// TODO: we probably should remove this once the PR is merged that has better data views for stories +const flightFieldNames: string[] = [ + 'AvgTicketPrice', + 'Cancelled', + 'Carrier', + 'dayOfWeek', + 'Dest', + 'DestAirportID', + 'DestCityName', + 'DestCountry', + 'DestLocation', + 'DestRegion', + 'DestWeather', + 'DistanceKilometers', + 'DistanceMiles', + 'FlightDelay', + 'FlightDelayMin', + 'FlightDelayType', + 'FlightNum', + 'FlightTimeHour', + 'FlightTimeMin', + 'Origin', + 'OriginAirportID', + 'OriginCityName', + 'OriginCountry', + 'OriginLocation', + 'OriginRegion', + 'OriginWeather', + 'timestamp', +]; +const flightFieldByName: { [key: string]: DataViewField } = {}; +flightFieldNames.forEach( + (flightFieldName) => + (flightFieldByName[flightFieldName] = { + name: flightFieldName, + type: 'string', + } as unknown as DataViewField) +); + +// Change some types manually for now +flightFieldByName.Cancelled = { name: 'Cancelled', type: 'boolean' } as DataViewField; +flightFieldByName.timestamp = { name: 'timestamp', type: 'date' } as DataViewField; + +const flightFields: DataViewField[] = Object.values(flightFieldByName); +const storybookFlightsDataView: DataView = { + id: 'demoDataFlights', + title: 'demo data flights', + fields: flightFields as unknown as IIndexPatternFieldList, + getFieldByName: (name: string) => flightFieldByName[name], +} as unknown as DataView; + +export default { + component: FieldPicker, + title: 'Field Picker', +}; + +export const FieldPickerWithDataView = () => { + return ; +}; + +export const FieldPickerWithFilter = () => { + return ( + { + // Only show fields with "Dest" in the title + return f.name.includes('Dest'); + }} + /> + ); +}; + +export const FieldPickerWithoutIndexPattern = () => { + return ; +}; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx new file mode 100644 index 0000000000000..bbdf389ccee14 --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { sortBy, uniq } from 'lodash'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DataView, DataViewField } from '../../../../data_views/common'; +import { FieldIcon, FieldButton } from '../../../../kibana_react/public'; + +import { FieldSearch } from './field_search'; + +import './field_picker.scss'; + +export interface Props { + dataView: DataView | null; + filterPredicate?: (f: DataViewField) => boolean; +} + +export const FieldPicker = ({ dataView, filterPredicate }: Props) => { + const [nameFilter, setNameFilter] = useState(''); + const [typesFilter, setTypesFilter] = useState([]); + const [selectedField, setSelectedField] = useState(null); + + // Retrieve, filter, and sort fields from data view + const fields = dataView + ? sortBy( + dataView.fields + .filter( + (f) => + f.name.includes(nameFilter) && + (typesFilter.length === 0 || typesFilter.includes(f.type as string)) + ) + .filter((f) => (filterPredicate ? filterPredicate(f) : true)), + ['name'] + ) + : []; + + const uniqueTypes = dataView ? uniq(dataView.fields.map((f) => f.type as string)) : []; + + return ( + + + setNameFilter(val)} + searchValue={nameFilter} + onFieldTypesChange={(types) => setTypesFilter(types)} + fieldTypesValue={typesFilter} + availableFieldTypes={uniqueTypes} + /> + + + + {fields.length > 0 && ( + + {fields.map((f, i) => { + return ( + + setSelectedField(f)} + isActive={f.name === selectedField?.name} + fieldName={f.name} + fieldIcon={} + /> + + ); + })} + + )} + {!dataView && ( + + + + + + + + )} + {dataView && fields.length === 0 && ( + + + + + + + + )} + + + {selectedField && ( + + +

+ +

+
+
+ + } + /> +
+
+ )} +
+ ); +}; diff --git a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx new file mode 100644 index 0000000000000..d3c6c728b3d08 --- /dev/null +++ b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFieldSearch, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiOutsideClickDetector, + EuiFilterButton, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FieldIcon } from '../../../../kibana_react/public'; + +export interface Props { + onSearchChange: (value: string) => void; + searchValue?: string; + + onFieldTypesChange: (value: string[]) => void; + fieldTypesValue: string[]; + + availableFieldTypes: string[]; +} + +export function FieldSearch({ + onSearchChange, + searchValue, + onFieldTypesChange, + fieldTypesValue, + availableFieldTypes, +}: Props) { + const searchPlaceholder = i18n.translate('presentationUtil.fieldSearch.searchPlaceHolder', { + defaultMessage: 'Search field names', + }); + + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const handleFilterButtonClicked = () => { + setPopoverOpen(!isPopoverOpen); + }; + + const buttonContent = ( + 0} + numFilters={0} + hasActiveFilters={fieldTypesValue.length > 0} + numActiveFilters={fieldTypesValue.length} + onClick={handleFilterButtonClicked} + > + + + ); + + return ( + + + + onSearchChange(event.currentTarget.value)} + placeholder={searchPlaceholder} + value={searchValue} + /> + + + + {}} isDisabled={!isPopoverOpen}> + + { + setPopoverOpen(false); + }} + button={buttonContent} + > + ( + { + if (fieldTypesValue.includes(type)) { + onFieldTypesChange(fieldTypesValue.filter((f) => f !== type)); + } else { + onFieldTypesChange([...fieldTypesValue, type]); + } + }} + > + + + {type} + + + ))} + /> + + + + + ); +} From a8e16ba9aa793a07074054a390e56d5d4b49051a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 22 Oct 2021 02:14:03 +0100 Subject: [PATCH 02/41] chore(NA): upgrades lmdb-store to v1.6.11 (#115971) * chore(NA): upgrades lmdb-store to v1.6.10 * chore(NA): upgrade into v1.6.11 --- package.json | 2 +- yarn.lock | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 3df7b65315a85..e6b17783197bc 100644 --- a/package.json +++ b/package.json @@ -743,7 +743,7 @@ "jsondiffpatch": "0.4.1", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^1.6.8", + "lmdb-store": "^1.6.11", "marge": "^1.0.1", "micromatch": "3.1.10", "minimist": "^1.2.5", diff --git a/yarn.lock b/yarn.lock index 86c4c9801f56e..669f8321fb4fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19212,18 +19212,17 @@ listr@^0.14.1: p-map "^2.0.0" rxjs "^6.3.3" -lmdb-store@^1.6.8: - version "1.6.8" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-1.6.8.tgz#f57c1fa4a8e8e7a73d58523d2bfbcee96782311f" - integrity sha512-Ltok13VVAfgO5Fdj/jVzXjPJZjefl1iENEHerZyAfAlzFUhvOrA73UdKItqmEPC338U29mm56ZBQr5NJQiKXow== +lmdb-store@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-1.6.11.tgz#801da597af8c7a01c81f87d5cc7a7497e381236d" + integrity sha512-hIvoGmHGsFhb2VRCmfhodA/837ULtJBwRHSHKIzhMB7WtPH6BRLPsvXp1MwD3avqGzuZfMyZDUp3tccLvr721Q== dependencies: - mkdirp "^1.0.4" nan "^2.14.2" node-gyp-build "^4.2.3" ordered-binary "^1.0.0" weak-lru-cache "^1.0.0" optionalDependencies: - msgpackr "^1.3.7" + msgpackr "^1.4.7" load-bmfont@^1.3.1, load-bmfont@^1.4.0: version "1.4.0" @@ -20672,7 +20671,7 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msgpackr-extract@^1.0.13: +msgpackr-extract@^1.0.14: version "1.0.14" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.14.tgz#87d3fe825d226e7f3d9fe136375091137f958561" integrity sha512-t8neMf53jNZRF+f0H9VvEUVvtjGZ21odSBRmFfjZiyxr9lKYY0mpY3kSWZAIc7YWXtCZGOvDQVx2oqcgGiRBrw== @@ -20680,12 +20679,12 @@ msgpackr-extract@^1.0.13: nan "^2.14.2" node-gyp-build "^4.2.3" -msgpackr@^1.3.7: - version "1.4.2" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.4.2.tgz#52ddf0130ccdb1067957fe61c8be828e82bb29ce" - integrity sha512-6gvaU+3xIflium8eJcruT66kLQr14lgTEmXtDm7KKzBSWHljD7pqu3VBQv1PDipFD5UGXLTIxGg5hGbO/jTvxQ== +msgpackr@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.4.7.tgz#d802ade841e7d2e873000b491cdda6574a3d5748" + integrity sha512-bhC8Ed1au3L3oHaR/fe4lk4w7PLGFcWQ5XY/Tk9N6tzDRz8YndjCG68TD8zcvYZoxNtw767eF/7VpaTpU9kf9w== optionalDependencies: - msgpackr-extract "^1.0.13" + msgpackr-extract "^1.0.14" multicast-dns-service-types@^1.1.0: version "1.1.0" From 286eacc79b919a2e16744dd4e7b90b8002e281a5 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 21 Oct 2021 19:34:19 -0700 Subject: [PATCH 03/41] [Alerting] Telemetry fix for min/max number of actions a rule has associated with (#115496) * [Alerting] Telemetry for max number of actions a rule has and max alerts created on execution * - * Update rules_client.ts * fixed task data * added unit test * fixed by adding runtime field * fixed task data * fixed test * fixed telemetry for throttle and interval * fixed task data * fixed task data * fixed test * fixed due to comments * fixed typecheck * fixed test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/usage/alerts_telemetry.test.ts | 33 +- .../alerting/server/usage/alerts_telemetry.ts | 324 ++++++------------ 2 files changed, 128 insertions(+), 229 deletions(-) diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index 348036252817d..03a96d19b8e8a 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -52,7 +52,7 @@ Object { `); }); - test('getTotalCountAggregations should return aggregations for throttle, interval and associated actions', async () => { + test('getTotalCountAggregations should return min/max connectors in use', async () => { const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; mockEsClient.search.mockReturnValue( // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values @@ -65,18 +65,17 @@ Object { 'logs.alert.document.count': 1, 'document.test.': 1, }, - namespaces: { - default: 1, - }, - }, - }, - throttleTime: { value: { min: 0, max: 10, totalCount: 10, totalSum: 20 } }, - intervalTime: { value: { min: 0, max: 2, totalCount: 2, totalSum: 5 } }, - connectorsAgg: { - connectors: { - value: { min: 0, max: 5, totalActionsCount: 10, totalAlertsCount: 2 }, }, }, + max_throttle_time: { value: 60 }, + min_throttle_time: { value: 0 }, + avg_throttle_time: { value: 30 }, + max_interval_time: { value: 10 }, + min_interval_time: { value: 1 }, + avg_interval_time: { value: 4.5 }, + max_actions_count: { value: 4 }, + min_actions_count: { value: 0 }, + avg_actions_count: { value: 2.5 }, }, hits: { hits: [], @@ -92,7 +91,7 @@ Object { Object { "connectors_per_alert": Object { "avg": 2.5, - "max": 5, + "max": 4, "min": 0, }, "count_by_type": Object { @@ -103,13 +102,13 @@ Object { "count_rules_namespaces": 0, "count_total": 4, "schedule_time": Object { - "avg": 2.5, - "max": 2, - "min": 0, + "avg": 4.5, + "max": 10, + "min": 1, }, "throttle_time": Object { - "avg": 2, - "max": 10, + "avg": 30, + "max": 60, "min": 0, }, } diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index ede2ac3613296..7ff9538c1aa26 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -52,219 +52,128 @@ export async function getTotalCountAggregations( | 'count_rules_namespaces' > > { - const throttleTimeMetric = { - scripted_metric: { - init_script: 'state.min = 0; state.max = 0; state.totalSum = 0; state.totalCount = 0;', - map_script: ` - if (doc['alert.throttle'].size() > 0) { - def throttle = doc['alert.throttle'].value; + const { body: results } = await esClient.search({ + index: kibanaInex, + body: { + size: 0, + query: { + bool: { + filter: [{ term: { type: 'alert' } }], + }, + }, + runtime_mappings: { + alert_action_count: { + type: 'long', + script: { + source: ` + def alert = params._source['alert']; + if (alert != null) { + def actions = alert.actions; + if (actions != null) { + emit(actions.length); + } else { + emit(0); + } + }`, + }, + }, + alert_interval: { + type: 'long', + script: { + source: ` + int parsed = 0; + if (doc['alert.schedule.interval'].size() > 0) { + def interval = doc['alert.schedule.interval'].value; - if (throttle.length() > 1) { - // get last char - String timeChar = throttle.substring(throttle.length() - 1); - // remove last char - throttle = throttle.substring(0, throttle.length() - 1); + if (interval.length() > 1) { + // get last char + String timeChar = interval.substring(interval.length() - 1); + // remove last char + interval = interval.substring(0, interval.length() - 1); - if (throttle.chars().allMatch(Character::isDigit)) { - // using of regex is not allowed in painless language - int parsed = Integer.parseInt(throttle); + if (interval.chars().allMatch(Character::isDigit)) { + // using of regex is not allowed in painless language + parsed = Integer.parseInt(interval); - if (timeChar.equals("s")) { - parsed = parsed; - } else if (timeChar.equals("m")) { - parsed = parsed * 60; - } else if (timeChar.equals("h")) { - parsed = parsed * 60 * 60; - } else if (timeChar.equals("d")) { - parsed = parsed * 24 * 60 * 60; - } - if (state.min === 0 || parsed < state.min) { - state.min = parsed; - } - if (parsed > state.max) { - state.max = parsed; + if (timeChar.equals("s")) { + parsed = parsed; + } else if (timeChar.equals("m")) { + parsed = parsed * 60; + } else if (timeChar.equals("h")) { + parsed = parsed * 60 * 60; + } else if (timeChar.equals("d")) { + parsed = parsed * 24 * 60 * 60; + } + emit(parsed); + } } - state.totalSum += parsed; - state.totalCount++; } - } - } - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalSum = 0; - long totalCount = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - totalSum += m.totalSum; - totalCount += m.totalCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.totalSum = totalSum; - result.totalCount = totalCount; - return result; - `, - }, - }; - - const intervalTimeMetric = { - scripted_metric: { - init_script: 'state.min = 0; state.max = 0; state.totalSum = 0; state.totalCount = 0;', - map_script: ` - if (doc['alert.schedule.interval'].size() > 0) { - def interval = doc['alert.schedule.interval'].value; + emit(parsed); + `, + }, + }, + alert_throttle: { + type: 'long', + script: { + source: ` + int parsed = 0; + if (doc['alert.throttle'].size() > 0) { + def throttle = doc['alert.throttle'].value; - if (interval.length() > 1) { - // get last char - String timeChar = interval.substring(interval.length() - 1); - // remove last char - interval = interval.substring(0, interval.length() - 1); + if (throttle.length() > 1) { + // get last char + String timeChar = throttle.substring(throttle.length() - 1); + // remove last char + throttle = throttle.substring(0, throttle.length() - 1); - if (interval.chars().allMatch(Character::isDigit)) { - // using of regex is not allowed in painless language - int parsed = Integer.parseInt(interval); + if (throttle.chars().allMatch(Character::isDigit)) { + // using of regex is not allowed in painless language + parsed = Integer.parseInt(throttle); - if (timeChar.equals("s")) { - parsed = parsed; - } else if (timeChar.equals("m")) { - parsed = parsed * 60; - } else if (timeChar.equals("h")) { - parsed = parsed * 60 * 60; - } else if (timeChar.equals("d")) { - parsed = parsed * 24 * 60 * 60; - } - if (state.min === 0 || parsed < state.min) { - state.min = parsed; - } - if (parsed > state.max) { - state.max = parsed; - } - state.totalSum += parsed; - state.totalCount++; + if (timeChar.equals("s")) { + parsed = parsed; + } else if (timeChar.equals("m")) { + parsed = parsed * 60; + } else if (timeChar.equals("h")) { + parsed = parsed * 60 * 60; + } else if (timeChar.equals("d")) { + parsed = parsed * 24 * 60 * 60; + } + emit(parsed); + } } - } - } - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalSum = 0; - long totalCount = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - totalSum += m.totalSum; - totalCount += m.totalCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.totalSum = totalSum; - result.totalCount = totalCount; - return result; - `, - }, - }; - - const connectorsMetric = { - scripted_metric: { - init_script: - 'state.currentAlertActions = 0; state.min = 0; state.max = 0; state.totalActionsCount = 0;', - map_script: ` - String refName = doc['alert.actions.actionRef'].value; - if (refName == 'action_0') { - if (state.currentAlertActions !== 0 && state.currentAlertActions < state.min) { - state.min = state.currentAlertActions; - } - if (state.currentAlertActions !== 0 && state.currentAlertActions > state.max) { - state.max = state.currentAlertActions; - } - state.currentAlertActions = 1; - } else { - state.currentAlertActions++; - } - state.totalActionsCount++; - `, - // Combine script is executed per cluster, but we already have a key-value pair per cluster. - // Despite docs that say this is optional, this script can't be blank. - combine_script: 'return state', - // Reduce script is executed across all clusters, so we need to add up all the total from each cluster - // This also needs to account for having no data - reduce_script: ` - double min = 0; - double max = 0; - long totalActionsCount = 0; - long currentAlertActions = 0; - for (Map m : states.toArray()) { - if (m !== null) { - min = min > 0 ? Math.min(min, m.min) : m.min; - max = Math.max(max, m.max); - currentAlertActions += m.currentAlertActions; - totalActionsCount += m.totalActionsCount; - } - } - Map result = new HashMap(); - result.min = min; - result.max = max; - result.currentAlertActions = currentAlertActions; - result.totalActionsCount = totalActionsCount; - return result; - `, - }, - }; - - const { body: results } = await esClient.search({ - index: kibanaInex, - size: 0, - body: { - query: { - bool: { - filter: [{ term: { type: 'alert' } }], + } + emit(parsed); + `, + }, }, }, aggs: { byAlertTypeId: alertTypeMetric, - throttleTime: throttleTimeMetric, - intervalTime: intervalTimeMetric, - connectorsAgg: { - nested: { - path: 'alert.actions', - }, - aggs: { - connectors: connectorsMetric, - }, - }, + max_throttle_time: { max: { field: 'alert_throttle' } }, + min_throttle_time: { min: { field: 'alert_throttle' } }, + avg_throttle_time: { avg: { field: 'alert_throttle' } }, + max_interval_time: { max: { field: 'alert_interval' } }, + min_interval_time: { min: { field: 'alert_interval' } }, + avg_interval_time: { avg: { field: 'alert_interval' } }, + max_actions_count: { max: { field: 'alert_action_count' } }, + min_actions_count: { min: { field: 'alert_action_count' } }, + avg_actions_count: { avg: { field: 'alert_action_count' } }, }, }, }); const aggregations = results.aggregations as { byAlertTypeId: { value: { ruleTypes: Record } }; - throttleTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; - intervalTime: { value: { min: number; max: number; totalCount: number; totalSum: number } }; - connectorsAgg: { - connectors: { - value: { min: number; max: number; totalActionsCount: number; totalAlertsCount: number }; - }; - }; + max_throttle_time: { value: number }; + min_throttle_time: { value: number }; + avg_throttle_time: { value: number }; + max_interval_time: { value: number }; + min_interval_time: { value: number }; + avg_interval_time: { value: number }; + max_actions_count: { value: number }; + min_actions_count: { value: number }; + avg_actions_count: { value: number }; }; const totalAlertsCount = Object.keys(aggregations.byAlertTypeId.value.ruleTypes).reduce( @@ -285,28 +194,19 @@ export async function getTotalCountAggregations( {} ), throttle_time: { - min: aggregations.throttleTime.value.min, - avg: - aggregations.throttleTime.value.totalCount > 0 - ? aggregations.throttleTime.value.totalSum / aggregations.throttleTime.value.totalCount - : 0, - max: aggregations.throttleTime.value.max, + min: aggregations.min_throttle_time.value, + avg: aggregations.avg_throttle_time.value, + max: aggregations.max_throttle_time.value, }, schedule_time: { - min: aggregations.intervalTime.value.min, - avg: - aggregations.intervalTime.value.totalCount > 0 - ? aggregations.intervalTime.value.totalSum / aggregations.intervalTime.value.totalCount - : 0, - max: aggregations.intervalTime.value.max, + min: aggregations.min_interval_time.value, + avg: aggregations.avg_interval_time.value, + max: aggregations.max_interval_time.value, }, connectors_per_alert: { - min: aggregations.connectorsAgg.connectors.value.min, - avg: - totalAlertsCount > 0 - ? aggregations.connectorsAgg.connectors.value.totalActionsCount / totalAlertsCount - : 0, - max: aggregations.connectorsAgg.connectors.value.max, + min: aggregations.min_actions_count.value, + avg: aggregations.avg_actions_count.value, + max: aggregations.max_actions_count.value, }, count_rules_namespaces: 0, }; From 776ad4896b04730265b0bb065a621927a8f52b0b Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 21 Oct 2021 20:23:21 -0700 Subject: [PATCH 04/41] Removing tests that support legacy exports (#116011) * deleted 2 files * removing the references of the deleted file --- test/functional/apps/dashboard/bwc_import.ts | 43 ------------- test/functional/apps/dashboard/index.ts | 2 - test/functional/apps/dashboard/time_zones.ts | 68 -------------------- 3 files changed, 113 deletions(-) delete mode 100644 test/functional/apps/dashboard/bwc_import.ts delete mode 100644 test/functional/apps/dashboard/time_zones.ts diff --git a/test/functional/apps/dashboard/bwc_import.ts b/test/functional/apps/dashboard/bwc_import.ts deleted file mode 100644 index ebb9d2b99ffa7..0000000000000 --- a/test/functional/apps/dashboard/bwc_import.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import path from 'path'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['dashboard', 'header', 'settings', 'savedObjects', 'common']); - const dashboardExpect = getService('dashboardExpect'); - // Legacy imports are no longer supported https://github.com/elastic/kibana/issues/103921 - describe.skip('bwc import', function describeIndexTests() { - before(async function () { - await PageObjects.dashboard.initTests(); - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', 'dashboard_6_0_1.json') - ); - await PageObjects.settings.associateIndexPattern( - 'dd684000-8255-11eb-a5e7-93c302c8f329', - 'logstash-*' - ); - await PageObjects.savedObjects.clickConfirmChanges(); - await PageObjects.savedObjects.clickImportDone(); - await PageObjects.common.navigateToApp('dashboard'); - }); - - describe('6.0.1 dashboard', () => { - it('loads an imported dashboard', async function () { - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.loadSavedDashboard('My custom bwc dashboard'); - await PageObjects.header.waitUntilLoadingHasFinished(); - - await dashboardExpect.metricValuesExist(['14,004']); - }); - }); - }); -} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 8627a258869bb..c9a62447f223a 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -49,7 +49,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_unsaved_state')); loadTestFile(require.resolve('./dashboard_unsaved_listing')); loadTestFile(require.resolve('./edit_visualizations')); - loadTestFile(require.resolve('./time_zones')); loadTestFile(require.resolve('./dashboard_options')); loadTestFile(require.resolve('./data_shared_attributes')); loadTestFile(require.resolve('./share')); @@ -95,7 +94,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_time_picker')); loadTestFile(require.resolve('./bwc_shared_urls')); - loadTestFile(require.resolve('./bwc_import')); loadTestFile(require.resolve('./panel_replacing')); loadTestFile(require.resolve('./panel_cloning')); loadTestFile(require.resolve('./copy_panel_to')); diff --git a/test/functional/apps/dashboard/time_zones.ts b/test/functional/apps/dashboard/time_zones.ts deleted file mode 100644 index f60792b3f292a..0000000000000 --- a/test/functional/apps/dashboard/time_zones.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import path from 'path'; -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const pieChart = getService('pieChart'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects([ - 'dashboard', - 'timePicker', - 'settings', - 'common', - 'savedObjects', - ]); - // Legacy imports are no longer supported https://github.com/elastic/kibana/issues/103921 - describe.skip('dashboard time zones', function () { - this.tags('includeFirefox'); - - before(async () => { - await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana'); - await kibanaServer.uiSettings.replace({ - defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', - }); - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', 'timezonetest_6_2_4.json') - ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - await PageObjects.dashboard.loadSavedDashboard('time zone test'); - }); - - after(async () => { - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); - }); - - it('Exported dashboard adjusts EST time to UTC', async () => { - const time = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(time.start).to.be('Apr 10, 2018 @ 03:00:00.000'); - expect(time.end).to.be('Apr 10, 2018 @ 04:00:00.000'); - await pieChart.expectPieSliceCount(4); - }); - - it('Changing timezone changes dashboard timestamp and shows the same data', async () => { - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'Etc/GMT+5'); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('time zone test'); - const time = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(time.start).to.be('Apr 9, 2018 @ 22:00:00.000'); - expect(time.end).to.be('Apr 9, 2018 @ 23:00:00.000'); - await pieChart.expectPieSliceCount(4); - }); - }); -} From 78a91f7595cd1879ca2f2b4ef0bd331380dce694 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Fri, 22 Oct 2021 07:14:45 -0600 Subject: [PATCH 05/41] Remove migrations.enableV2 in 8.0 (#116023) --- .../saved_objects_config.test.ts | 44 ------------------- .../saved_objects/saved_objects_config.ts | 4 -- 2 files changed, 48 deletions(-) delete mode 100644 src/core/server/saved_objects/saved_objects_config.test.ts diff --git a/src/core/server/saved_objects/saved_objects_config.test.ts b/src/core/server/saved_objects/saved_objects_config.test.ts deleted file mode 100644 index 06b9e9661b746..0000000000000 --- a/src/core/server/saved_objects/saved_objects_config.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { savedObjectsMigrationConfig } from './saved_objects_config'; -import { getDeprecationsFor } from '../config/test_utils'; - -const applyMigrationsDeprecations = (settings: Record = {}) => - getDeprecationsFor({ - provider: savedObjectsMigrationConfig.deprecations!, - settings, - path: 'migrations', - }); - -describe('migrations config', function () { - describe('deprecations', () => { - it('logs a warning if migrations.enableV2 is set: true', () => { - const { messages } = applyMigrationsDeprecations({ enableV2: true }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "You no longer need to configure \\"migrations.enableV2\\".", - ] - `); - }); - - it('logs a warning if migrations.enableV2 is set: false', () => { - const { messages } = applyMigrationsDeprecations({ enableV2: false }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "You no longer need to configure \\"migrations.enableV2\\".", - ] - `); - }); - }); - - it('does not log a warning if migrations.enableV2 is not set', () => { - const { messages } = applyMigrationsDeprecations({ batchSize: 1_000 }); - expect(messages).toMatchInlineSnapshot(`Array []`); - }); -}); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 02fbd974da4ae..e5dc64186f66d 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -7,7 +7,6 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { ConfigDeprecationProvider } from '../config'; import type { ServiceConfigDescriptor } from '../internal_types'; const migrationSchema = schema.object({ @@ -21,13 +20,10 @@ const migrationSchema = schema.object({ export type SavedObjectsMigrationConfigType = TypeOf; -const migrationDeprecations: ConfigDeprecationProvider = ({ unused }) => [unused('enableV2')]; - export const savedObjectsMigrationConfig: ServiceConfigDescriptor = { path: 'migrations', schema: migrationSchema, - deprecations: migrationDeprecations, }; const soSchema = schema.object({ From 003c0f36ec178dcde1d36679e49477a27065c1fa Mon Sep 17 00:00:00 2001 From: Sandra G Date: Fri, 22 Oct 2021 14:17:12 -0400 Subject: [PATCH 06/41] don't request when request is pending (#115999) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/pages/page_template.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index c0030cfcfe55c..a508714612c28 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -49,6 +49,7 @@ export const PageTemplate: React.FC = ({ const { currentTimerange } = useContext(MonitoringTimeContainer.Context); const [loaded, setLoaded] = useState(false); + const [isRequestPending, setIsRequestPending] = useState(false); const history = useHistory(); const [hasError, setHasError] = useState(false); const handleRequestError = useRequestErrorHandler(); @@ -62,6 +63,7 @@ export const PageTemplate: React.FC = ({ ); useEffect(() => { + setIsRequestPending(true); getPageData?.() .then(getPageDataResponseHandler) .catch((err: IHttpFetchError) => { @@ -70,11 +72,20 @@ export const PageTemplate: React.FC = ({ }) .finally(() => { setLoaded(true); + setIsRequestPending(false); }); }, [getPageData, currentTimerange, getPageDataResponseHandler, handleRequestError]); const onRefresh = () => { - getPageData?.().then(getPageDataResponseHandler).catch(handleRequestError); + // don't refresh when a request is pending + if (isRequestPending) return; + setIsRequestPending(true); + getPageData?.() + .then(getPageDataResponseHandler) + .catch(handleRequestError) + .finally(() => { + setIsRequestPending(false); + }); if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { updateSetupModeData(); From 110a8418f9568624ab2e1764e2ed78d16ee3d9a7 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sat, 23 Oct 2021 23:54:21 +0200 Subject: [PATCH 07/41] [APM] Add live mode to synthtrace (#115988) --- packages/elastic-apm-generator/BUILD.bazel | 2 + packages/elastic-apm-generator/README.md | 28 +++-- .../elastic-apm-generator/src/.eslintrc.js | 13 ++ .../elastic-apm-generator/src/lib/interval.ts | 2 +- .../src/lib/output/to_elasticsearch_output.ts | 22 +++- .../elastic-apm-generator/src/scripts/es.ts | 113 ----------------- .../src/scripts/examples/01_simple_trace.ts | 4 +- .../src/scripts/{es.js => run.js} | 2 +- .../elastic-apm-generator/src/scripts/run.ts | 117 ++++++++++++++++++ .../src/scripts/utils/clean_write_targets.ts | 63 ++++++++++ .../src/scripts/utils/common_options.ts | 53 ++++++++ .../src/scripts/utils/get_common_resources.ts | 80 ++++++++++++ .../src/scripts/utils/get_scenario.ts | 25 ++++ .../src/scripts/utils/get_write_targets.ts | 56 +++++++++ .../src/scripts/utils/interval_to_ms.ts | 31 +++++ .../src/scripts/utils/logger.ts | 32 +++++ .../utils/start_historical_data_upload.ts | 64 ++++++++++ .../scripts/utils/start_live_data_upload.ts | 75 +++++++++++ .../src/scripts/utils/upload_events.ts | 72 +++++++++++ .../test/scenarios/01_simple_trace.test.ts | 2 +- .../scenarios/02_transaction_metrics.test.ts | 2 +- .../03_span_destination_metrics.test.ts | 2 +- .../scenarios/04_breakdown_metrics.test.ts | 2 +- .../src/test/to_elasticsearch_output.test.ts | 11 +- .../apm_api_integration/common/trace_data.ts | 13 +- 25 files changed, 750 insertions(+), 136 deletions(-) create mode 100644 packages/elastic-apm-generator/src/.eslintrc.js delete mode 100644 packages/elastic-apm-generator/src/scripts/es.ts rename packages/elastic-apm-generator/src/scripts/{es.js => run.js} (96%) create mode 100644 packages/elastic-apm-generator/src/scripts/run.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/common_options.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/logger.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts create mode 100644 packages/elastic-apm-generator/src/scripts/utils/upload_events.ts diff --git a/packages/elastic-apm-generator/BUILD.bazel b/packages/elastic-apm-generator/BUILD.bazel index 6b46b2b9181e5..396c27b3a4c89 100644 --- a/packages/elastic-apm-generator/BUILD.bazel +++ b/packages/elastic-apm-generator/BUILD.bazel @@ -25,6 +25,7 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "//packages/elastic-datemath", "@npm//@elastic/elasticsearch", "@npm//lodash", "@npm//moment", @@ -36,6 +37,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ + "//packages/elastic-datemath", "@npm//@elastic/elasticsearch", "@npm//moment", "@npm//p-limit", diff --git a/packages/elastic-apm-generator/README.md b/packages/elastic-apm-generator/README.md index e43187a8155d3..b442c0ec23ee0 100644 --- a/packages/elastic-apm-generator/README.md +++ b/packages/elastic-apm-generator/README.md @@ -11,7 +11,7 @@ This section assumes that you've installed Kibana's dependencies by running `yar This library can currently be used in two ways: - Imported as a Node.js module, for instance to be used in Kibana's functional test suite. -- With a command line interface, to index data based on some example scenarios. +- With a command line interface, to index data based on a specified scenario. ### Using the Node.js module @@ -32,7 +32,7 @@ const instance = service('synth-go', 'production', 'go') .instance('instance-a'); const from = new Date('2021-01-01T12:00:00.000Z').getTime(); -const to = new Date('2021-01-01T12:00:00.000Z').getTime() - 1; +const to = new Date('2021-01-01T12:00:00.000Z').getTime(); const traceEvents = timerange(from, to) .interval('1m') @@ -82,12 +82,26 @@ const esEvents = toElasticsearchOutput([ ### CLI -Via the CLI, you can upload examples. The supported examples are listed in `src/lib/es.ts`. A `--target` option that specifies the Elasticsearch URL should be defined when running the `example` command. Here's an example: +Via the CLI, you can upload scenarios, either using a fixed time range or continuously generating data. Some examples are available in in `src/scripts/examples`. Here's an example for live data: -`$ node packages/elastic-apm-generator/src/scripts/es.js example simple-trace --target=http://admin:changeme@localhost:9200` +`$ node packages/elastic-apm-generator/src/scripts/run packages/elastic-apm-generator/src/examples/01_simple_trace.ts --target=http://admin:changeme@localhost:9200 --live` + +For a fixed time window: +`$ node packages/elastic-apm-generator/src/scripts/run packages/elastic-apm-generator/src/examples/01_simple_trace.ts --target=http://admin:changeme@localhost:9200 --from=now-24h --to=now` + +The script will try to automatically find bootstrapped APM indices. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ The following options are supported: -- `to`: the end of the time range, in ISO format. By default, the current time will be used. -- `from`: the start of the time range, in ISO format. By default, `to` minus 15 minutes will be used. -- `apm-server-version`: the version used in the index names bootstrapped by APM Server, e.g. `7.16.0`. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ +| Option | Description | Default | +| -------------- | ------------------------------------------------------- | ------------ | +| `--from` | The start of the time window. | `now - 15m` | +| `--to` | The end of the time window. | `now` | +| `--live` | Continously ingest data | `false` | +| `--bucketSize` | Size of bucket for which to generate data. | `15m` | +| `--clean` | Clean APM indices before indexing new data. | `false` | +| `--interval` | The interval at which to index data. | `10s` | +| `--logLevel` | Log level. | `info` | +| `--lookback` | The lookback window for which data should be generated. | `15m` | +| `--target` | Elasticsearch target, including username/password. | **Required** | +| `--workers` | Amount of simultaneously connected ES clients. | `1` | diff --git a/packages/elastic-apm-generator/src/.eslintrc.js b/packages/elastic-apm-generator/src/.eslintrc.js new file mode 100644 index 0000000000000..2e3eef95f4bf3 --- /dev/null +++ b/packages/elastic-apm-generator/src/.eslintrc.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + rules: { + 'import/no-default-export': 'off', + }, +}; diff --git a/packages/elastic-apm-generator/src/lib/interval.ts b/packages/elastic-apm-generator/src/lib/interval.ts index f13d54fd7415e..bafd1a06c5348 100644 --- a/packages/elastic-apm-generator/src/lib/interval.ts +++ b/packages/elastic-apm-generator/src/lib/interval.ts @@ -21,7 +21,7 @@ export class Interval { throw new Error('Failed to parse interval'); } const timestamps: number[] = []; - while (now <= this.to) { + while (now < this.to) { timestamps.push(...new Array(rate).fill(now)); now = moment(now) .add(Number(args[1]), args[2] as any) diff --git a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts index d90ce8e01f83d..31f3e8c8ed270 100644 --- a/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts +++ b/packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts @@ -10,7 +10,25 @@ import { set } from 'lodash'; import { getObserverDefaults } from '../..'; import { Fields } from '../entity'; -export function toElasticsearchOutput(events: Fields[], versionOverride?: string) { +export interface ElasticsearchOutput { + _index: string; + _source: unknown; +} + +export interface ElasticsearchOutputWriteTargets { + transaction: string; + span: string; + error: string; + metric: string; +} + +export function toElasticsearchOutput({ + events, + writeTargets, +}: { + events: Fields[]; + writeTargets: ElasticsearchOutputWriteTargets; +}): ElasticsearchOutput[] { return events.map((event) => { const values = { ...event, @@ -29,7 +47,7 @@ export function toElasticsearchOutput(events: Fields[], versionOverride?: string set(document, key, val); } return { - _index: `apm-${versionOverride || values['observer.version']}-${values['processor.event']}`, + _index: writeTargets[event['processor.event'] as keyof ElasticsearchOutputWriteTargets], _source: document, }; }); diff --git a/packages/elastic-apm-generator/src/scripts/es.ts b/packages/elastic-apm-generator/src/scripts/es.ts deleted file mode 100644 index d023ef7172892..0000000000000 --- a/packages/elastic-apm-generator/src/scripts/es.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { inspect } from 'util'; -import { Client } from '@elastic/elasticsearch'; -import { chunk } from 'lodash'; -import pLimit from 'p-limit'; -import yargs from 'yargs/yargs'; -import { toElasticsearchOutput } from '..'; -import { simpleTrace } from './examples/01_simple_trace'; - -yargs(process.argv.slice(2)) - .command( - 'example', - 'run an example scenario', - (y) => { - return y - .positional('scenario', { - describe: 'scenario to run', - choices: ['simple-trace'], - demandOption: true, - }) - .option('target', { - describe: 'elasticsearch target, including username/password', - }) - .option('from', { describe: 'start of timerange' }) - .option('to', { describe: 'end of timerange' }) - .option('workers', { - default: 1, - describe: 'number of concurrently connected ES clients', - }) - .option('apm-server-version', { - describe: 'APM Server version override', - }) - .demandOption('target'); - }, - (argv) => { - let events: any[] = []; - const toDateString = (argv.to as string | undefined) || new Date().toISOString(); - const fromDateString = - (argv.from as string | undefined) || - new Date(new Date(toDateString).getTime() - 15 * 60 * 1000).toISOString(); - - const to = new Date(toDateString).getTime(); - const from = new Date(fromDateString).getTime(); - - switch (argv._[1]) { - case 'simple-trace': - events = simpleTrace(from, to); - break; - } - - const docs = toElasticsearchOutput(events, argv['apm-server-version'] as string); - - const client = new Client({ - node: argv.target as string, - }); - - const fn = pLimit(argv.workers); - - const batches = chunk(docs, 1000); - - // eslint-disable-next-line no-console - console.log( - 'Uploading', - docs.length, - 'docs in', - batches.length, - 'batches', - 'from', - fromDateString, - 'to', - toDateString - ); - - Promise.all( - batches.map((batch) => - fn(() => { - return client.bulk({ - require_alias: true, - body: batch.flatMap((doc) => { - return [{ index: { _index: doc._index } }, doc._source]; - }), - }); - }) - ) - ) - .then((results) => { - const errors = results - .flatMap((result) => result.body.items) - .filter((item) => !!item.index?.error) - .map((item) => item.index?.error); - - if (errors.length) { - // eslint-disable-next-line no-console - console.error(inspect(errors.slice(0, 10), { depth: null })); - throw new Error('Failed to upload some items'); - } - process.exit(); - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.error(err); - process.exit(1); - }); - } - ) - .parse(); diff --git a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts index f6aad154532c2..6b857391b4f96 100644 --- a/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts +++ b/packages/elastic-apm-generator/src/scripts/examples/01_simple_trace.ts @@ -9,12 +9,12 @@ import { service, timerange, getTransactionMetrics, getSpanDestinationMetrics } from '../..'; import { getBreakdownMetrics } from '../../lib/utils/get_breakdown_metrics'; -export function simpleTrace(from: number, to: number) { +export default function ({ from, to }: { from: number; to: number }) { const instance = service('opbeans-go', 'production', 'go').instance('instance'); const range = timerange(from, to); - const transactionName = '240rpm/60% 1000ms'; + const transactionName = '240rpm/75% 1000ms'; const successfulTraceEvents = range .interval('1s') diff --git a/packages/elastic-apm-generator/src/scripts/es.js b/packages/elastic-apm-generator/src/scripts/run.js similarity index 96% rename from packages/elastic-apm-generator/src/scripts/es.js rename to packages/elastic-apm-generator/src/scripts/run.js index 9f99a5d19b8f8..426b247b6b623 100644 --- a/packages/elastic-apm-generator/src/scripts/es.js +++ b/packages/elastic-apm-generator/src/scripts/run.js @@ -12,4 +12,4 @@ require('@babel/register')({ presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], }); -require('./es.ts'); +require('./run.ts'); diff --git a/packages/elastic-apm-generator/src/scripts/run.ts b/packages/elastic-apm-generator/src/scripts/run.ts new file mode 100644 index 0000000000000..ad453ac96ff10 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/run.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import datemath from '@elastic/datemath'; +import yargs from 'yargs/yargs'; +import { cleanWriteTargets } from './utils/clean_write_targets'; +import { + bucketSizeOption, + cleanOption, + fileOption, + intervalOption, + targetOption, + workerOption, + logLevelOption, +} from './utils/common_options'; +import { intervalToMs } from './utils/interval_to_ms'; +import { getCommonResources } from './utils/get_common_resources'; +import { startHistoricalDataUpload } from './utils/start_historical_data_upload'; +import { startLiveDataUpload } from './utils/start_live_data_upload'; + +yargs(process.argv.slice(2)) + .command( + '*', + 'Generate data and index into Elasticsearch', + (y) => { + return y + .positional('file', fileOption) + .option('bucketSize', bucketSizeOption) + .option('workers', workerOption) + .option('interval', intervalOption) + .option('clean', cleanOption) + .option('target', targetOption) + .option('logLevel', logLevelOption) + .option('from', { + description: 'The start of the time window', + }) + .option('to', { + description: 'The end of the time window', + }) + .option('live', { + description: 'Generate and index data continuously', + boolean: true, + }) + .conflicts('to', 'live'); + }, + async (argv) => { + const { + scenario, + intervalInMs, + bucketSizeInMs, + target, + workers, + clean, + logger, + writeTargets, + client, + } = await getCommonResources(argv); + + if (clean) { + await cleanWriteTargets({ writeTargets, client, logger }); + } + + const to = datemath.parse(String(argv.to ?? 'now'))!.valueOf(); + const from = argv.from + ? datemath.parse(String(argv.from))!.valueOf() + : to - intervalToMs('15m'); + + const live = argv.live; + + logger.info( + `Starting data generation\n: ${JSON.stringify( + { + intervalInMs, + bucketSizeInMs, + workers, + target, + writeTargets, + from: new Date(from).toISOString(), + to: new Date(to).toISOString(), + live, + }, + null, + 2 + )}` + ); + + startHistoricalDataUpload({ + from, + to, + scenario, + intervalInMs, + bucketSizeInMs, + client, + workers, + writeTargets, + logger, + }); + + if (live) { + startLiveDataUpload({ + bucketSizeInMs, + client, + intervalInMs, + logger, + scenario, + start: to, + workers, + writeTargets, + }); + } + } + ) + .parse(); diff --git a/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts b/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts new file mode 100644 index 0000000000000..efa24f164d51e --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/clean_write_targets.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Client } from '@elastic/elasticsearch'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Logger } from './logger'; + +export async function cleanWriteTargets({ + writeTargets, + client, + logger, +}: { + writeTargets: ElasticsearchOutputWriteTargets; + client: Client; + logger: Logger; +}) { + const targets = Object.values(writeTargets); + + logger.info(`Cleaning indices: ${targets.join(', ')}`); + + const response = await client.deleteByQuery({ + index: targets, + allow_no_indices: true, + conflicts: 'proceed', + body: { + query: { + match_all: {}, + }, + }, + wait_for_completion: false, + }); + + const task = response.body.task; + + if (task) { + await new Promise((resolve, reject) => { + const pollForTaskCompletion = async () => { + const taskResponse = await client.tasks.get({ + task_id: String(task), + }); + + logger.debug( + `Polled for task:\n${JSON.stringify(taskResponse.body, ['completed', 'error'], 2)}` + ); + + if (taskResponse.body.completed) { + resolve(); + } else if (taskResponse.body.error) { + reject(taskResponse.body.error); + } else { + setTimeout(pollForTaskCompletion, 2500); + } + }; + + pollForTaskCompletion(); + }); + } +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/common_options.ts b/packages/elastic-apm-generator/src/scripts/utils/common_options.ts new file mode 100644 index 0000000000000..eba547114d533 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/common_options.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const fileOption = { + describe: 'File that contains the trace scenario', + demandOption: true, +}; + +const intervalOption = { + describe: 'The interval at which to index data', + default: '10s', +}; + +const targetOption = { + describe: 'Elasticsearch target, including username/password', + demandOption: true, +}; + +const bucketSizeOption = { + describe: 'Size of bucket for which to generate data', + default: '15m', +}; + +const workerOption = { + describe: 'Amount of simultaneously connected ES clients', + default: 1, +}; + +const cleanOption = { + describe: 'Clean APM indices before indexing new data', + default: false, + boolean: true as const, +}; + +const logLevelOption = { + describe: 'Log level', + default: 'info', +}; + +export { + fileOption, + intervalOption, + targetOption, + bucketSizeOption, + workerOption, + cleanOption, + logLevelOption, +}; diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts b/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts new file mode 100644 index 0000000000000..1288c1390e92c --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_common_resources.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Client } from '@elastic/elasticsearch'; +import { getScenario } from './get_scenario'; +import { getWriteTargets } from './get_write_targets'; +import { intervalToMs } from './interval_to_ms'; +import { createLogger, LogLevel } from './logger'; + +export async function getCommonResources({ + file, + interval, + bucketSize, + workers, + target, + clean, + logLevel, +}: { + file: unknown; + interval: unknown; + bucketSize: unknown; + workers: unknown; + target: unknown; + clean: boolean; + logLevel: unknown; +}) { + let parsedLogLevel = LogLevel.info; + switch (logLevel) { + case 'info': + parsedLogLevel = LogLevel.info; + break; + + case 'debug': + parsedLogLevel = LogLevel.debug; + break; + + case 'quiet': + parsedLogLevel = LogLevel.quiet; + break; + } + + const logger = createLogger(parsedLogLevel); + + const intervalInMs = intervalToMs(interval); + if (!intervalInMs) { + throw new Error('Invalid interval'); + } + + const bucketSizeInMs = intervalToMs(bucketSize); + + if (!bucketSizeInMs) { + throw new Error('Invalid bucket size'); + } + + const client = new Client({ + node: String(target), + }); + + const [scenario, writeTargets] = await Promise.all([ + getScenario({ file, logger }), + getWriteTargets({ client }), + ]); + + return { + scenario, + writeTargets, + logger, + client, + intervalInMs, + bucketSizeInMs, + workers: Number(workers), + target: String(target), + clean, + }; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts b/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts new file mode 100644 index 0000000000000..887969e8459cc --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_scenario.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import Path from 'path'; +import { Fields } from '../../lib/entity'; +import { Logger } from './logger'; + +export type Scenario = (options: { from: number; to: number }) => Fields[]; + +export function getScenario({ file, logger }: { file: unknown; logger: Logger }) { + const location = Path.join(process.cwd(), String(file)); + + logger.debug(`Loading scenario from ${location}`); + + return import(location).then((m) => { + if (m && m.default) { + return m.default; + } + throw new Error(`Could not find scenario at ${location}`); + }) as Promise; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts b/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts new file mode 100644 index 0000000000000..3640e4efaf796 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/get_write_targets.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Client } from '@elastic/elasticsearch'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; + +export async function getWriteTargets({ + client, +}: { + client: Client; +}): Promise { + const [indicesResponse, datastreamsResponse] = await Promise.all([ + client.indices.getAlias({ + index: 'apm-*', + }), + client.indices.getDataStream({ + name: '*apm', + }), + ]); + + function getDataStreamName(filter: string) { + return datastreamsResponse.body.data_streams.find((stream) => stream.name.includes(filter)) + ?.name; + } + + function getAlias(filter: string) { + return Object.keys(indicesResponse.body) + .map((key) => { + return { + key, + writeIndexAlias: Object.entries(indicesResponse.body[key].aliases).find( + ([_, alias]) => alias.is_write_index + )?.[0], + }; + }) + .find(({ key }) => key.includes(filter))?.writeIndexAlias!; + } + + const targets = { + transaction: getDataStreamName('traces-apm') || getAlias('-transaction'), + span: getDataStreamName('traces-apm') || getAlias('-span'), + metric: getDataStreamName('metrics-apm') || getAlias('-metric'), + error: getDataStreamName('logs-apm') || getAlias('-error'), + }; + + if (!targets.transaction || !targets.span || !targets.metric || !targets.error) { + throw new Error('Write targets could not be determined'); + } + + return targets; +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts b/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.ts new file mode 100644 index 0000000000000..4cba832be3161 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/interval_to_ms.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export function intervalToMs(interval: unknown) { + const [, valueAsString, unit] = String(interval).split(/(.*)(s|m|h|d|w)/); + + const value = Number(valueAsString); + + switch (unit) { + case 's': + return value * 1000; + case 'm': + return value * 1000 * 60; + + case 'h': + return value * 1000 * 60 * 60; + + case 'd': + return value * 1000 * 60 * 60 * 24; + + case 'w': + return value * 1000 * 60 * 60 * 24 * 7; + } + + throw new Error('Could not parse interval'); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/logger.ts b/packages/elastic-apm-generator/src/scripts/utils/logger.ts new file mode 100644 index 0000000000000..c9017cb08e663 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/logger.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export enum LogLevel { + debug = 0, + info = 1, + quiet = 2, +} + +export function createLogger(logLevel: LogLevel) { + return { + debug: (...args: any[]) => { + if (logLevel <= LogLevel.debug) { + // eslint-disable-next-line no-console + console.debug(...args); + } + }, + info: (...args: any[]) => { + if (logLevel <= LogLevel.info) { + // eslint-disable-next-line no-console + console.log(...args); + } + }, + }; +} + +export type Logger = ReturnType; diff --git a/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts b/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts new file mode 100644 index 0000000000000..db14090dd1d8f --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/start_historical_data_upload.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Client } from '@elastic/elasticsearch'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Scenario } from './get_scenario'; +import { Logger } from './logger'; +import { uploadEvents } from './upload_events'; + +export async function startHistoricalDataUpload({ + from, + to, + scenario, + intervalInMs, + bucketSizeInMs, + client, + workers, + writeTargets, + logger, +}: { + from: number; + to: number; + scenario: Scenario; + intervalInMs: number; + bucketSizeInMs: number; + client: Client; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + logger: Logger; +}) { + let requestedUntil: number = from; + function uploadNextBatch() { + const bucketFrom = requestedUntil; + const bucketTo = Math.min(to, bucketFrom + bucketSizeInMs); + + const events = scenario({ from: bucketFrom, to: bucketTo }); + + logger.info( + `Uploading: ${new Date(bucketFrom).toISOString()} to ${new Date(bucketTo).toISOString()}` + ); + + uploadEvents({ + events, + client, + workers, + writeTargets, + logger, + }).then(() => { + if (bucketTo >= to) { + return; + } + uploadNextBatch(); + }); + + requestedUntil = bucketTo; + } + + return uploadNextBatch(); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts b/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts new file mode 100644 index 0000000000000..bf330732f343e --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/start_live_data_upload.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Client } from '@elastic/elasticsearch'; +import { partition } from 'lodash'; +import { Fields } from '../../lib/entity'; +import { ElasticsearchOutputWriteTargets } from '../../lib/output/to_elasticsearch_output'; +import { Scenario } from './get_scenario'; +import { Logger } from './logger'; +import { uploadEvents } from './upload_events'; + +export function startLiveDataUpload({ + start, + bucketSizeInMs, + intervalInMs, + workers, + writeTargets, + scenario, + client, + logger, +}: { + start: number; + bucketSizeInMs: number; + intervalInMs: number; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + scenario: Scenario; + client: Client; + logger: Logger; +}) { + let queuedEvents: Fields[] = []; + let requestedUntil: number = start; + + function uploadNextBatch() { + const end = new Date().getTime(); + if (end > requestedUntil) { + const bucketFrom = requestedUntil; + const bucketTo = requestedUntil + bucketSizeInMs; + const nextEvents = scenario({ from: bucketFrom, to: bucketTo }); + logger.debug( + `Requesting ${new Date(bucketFrom).toISOString()} to ${new Date( + bucketTo + ).toISOString()}, events: ${nextEvents.length}` + ); + queuedEvents.push(...nextEvents); + requestedUntil = bucketTo; + } + + const [eventsToUpload, eventsToRemainInQueue] = partition( + queuedEvents, + (event) => event['@timestamp']! <= end + ); + + logger.info(`Uploading until ${new Date(end).toISOString()}, events: ${eventsToUpload.length}`); + + queuedEvents = eventsToRemainInQueue; + + uploadEvents({ + events: eventsToUpload, + client, + workers, + writeTargets, + logger, + }); + } + + setInterval(uploadNextBatch, intervalInMs); + + uploadNextBatch(); +} diff --git a/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts b/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts new file mode 100644 index 0000000000000..89cf4d4602177 --- /dev/null +++ b/packages/elastic-apm-generator/src/scripts/utils/upload_events.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { Client } from '@elastic/elasticsearch'; +import { chunk } from 'lodash'; +import pLimit from 'p-limit'; +import { inspect } from 'util'; +import { Fields } from '../../lib/entity'; +import { + ElasticsearchOutputWriteTargets, + toElasticsearchOutput, +} from '../../lib/output/to_elasticsearch_output'; +import { Logger } from './logger'; + +export function uploadEvents({ + events, + client, + workers, + writeTargets, + logger, +}: { + events: Fields[]; + client: Client; + workers: number; + writeTargets: ElasticsearchOutputWriteTargets; + logger: Logger; +}) { + const esDocuments = toElasticsearchOutput({ events, writeTargets }); + const fn = pLimit(workers); + + const batches = chunk(esDocuments, 5000); + + logger.debug(`Uploading ${esDocuments.length} in ${batches.length} batches`); + + const time = new Date().getTime(); + + return Promise.all( + batches.map((batch) => + fn(() => { + return client.bulk({ + require_alias: true, + body: batch.flatMap((doc) => { + return [{ index: { _index: doc._index } }, doc._source]; + }), + }); + }) + ) + ) + .then((results) => { + const errors = results + .flatMap((result) => result.body.items) + .filter((item) => !!item.index?.error) + .map((item) => item.index?.error); + + if (errors.length) { + // eslint-disable-next-line no-console + console.error(inspect(errors.slice(0, 10), { depth: null })); + throw new Error('Failed to upload some items'); + } + + logger.debug(`Uploaded ${events.length} in ${new Date().getTime() - time}ms`); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + }); +} diff --git a/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts b/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts index 733093ce0a71c..866a9745befc3 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/01_simple_trace.test.ts @@ -18,7 +18,7 @@ describe('simple trace', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = range diff --git a/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts index 0b9f192d3d27d..58b28f71b9afc 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/02_transaction_metrics.test.ts @@ -19,7 +19,7 @@ describe('transaction metrics', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = getTransactionMetrics( diff --git a/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts index 158ccc5b5e714..0bf59f044bf03 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/03_span_destination_metrics.test.ts @@ -19,7 +19,7 @@ describe('span destination metrics', () => { const range = timerange( new Date('2021-01-01T00:00:00.000Z').getTime(), - new Date('2021-01-01T00:15:00.000Z').getTime() - 1 + new Date('2021-01-01T00:15:00.000Z').getTime() ); events = getSpanDestinationMetrics( diff --git a/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts b/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts index aeb944f35faf6..469f56b99c5f2 100644 --- a/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts +++ b/packages/elastic-apm-generator/src/test/scenarios/04_breakdown_metrics.test.ts @@ -26,7 +26,7 @@ describe('breakdown metrics', () => { const start = new Date('2021-01-01T00:00:00.000Z').getTime(); - const range = timerange(start, start + INTERVALS * 30 * 1000 - 1); + const range = timerange(start, start + INTERVALS * 30 * 1000); events = getBreakdownMetrics([ ...range diff --git a/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts b/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts index c1a5d47654fc9..d15ea89083112 100644 --- a/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts +++ b/packages/elastic-apm-generator/src/test/to_elasticsearch_output.test.ts @@ -9,6 +9,13 @@ import { Fields } from '../lib/entity'; import { toElasticsearchOutput } from '../lib/output/to_elasticsearch_output'; +const writeTargets = { + transaction: 'apm-8.0.0-transaction', + span: 'apm-8.0.0-span', + metric: 'apm-8.0.0-metric', + error: 'apm-8.0.0-error', +}; + describe('output to elasticsearch', () => { let event: Fields; @@ -21,13 +28,13 @@ describe('output to elasticsearch', () => { }); it('properly formats @timestamp', () => { - const doc = toElasticsearchOutput([event])[0] as any; + const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source['@timestamp']).toEqual('2020-12-31T23:00:00.000Z'); }); it('formats a nested object', () => { - const doc = toElasticsearchOutput([event])[0] as any; + const doc = toElasticsearchOutput({ events: [event], writeTargets })[0] as any; expect(doc._source.processor).toEqual({ event: 'transaction', diff --git a/x-pack/test/apm_api_integration/common/trace_data.ts b/x-pack/test/apm_api_integration/common/trace_data.ts index 84bbb4beea4f4..9799e111cb135 100644 --- a/x-pack/test/apm_api_integration/common/trace_data.ts +++ b/x-pack/test/apm_api_integration/common/trace_data.ts @@ -20,15 +20,20 @@ export async function traceData(context: InheritedFtrProviderContext) { const es = context.getService('es'); return { index: (events: any[]) => { - const esEvents = toElasticsearchOutput( - [ + const esEvents = toElasticsearchOutput({ + events: [ ...events, ...getTransactionMetrics(events), ...getSpanDestinationMetrics(events), ...getBreakdownMetrics(events), ], - '7.14.0' - ); + writeTargets: { + transaction: 'apm-7.14.0-transaction', + span: 'apm-7.14.0-span', + error: 'apm-7.14.0-error', + metric: 'apm-7.14.0-metric', + }, + }); const batches = chunk(esEvents, 1000); const limiter = pLimit(1); From 4d3644030afce4a3463b0d390e32293337382889 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 25 Oct 2021 10:34:36 +0300 Subject: [PATCH 08/41] [Connectors] Check connector's responses (#115797) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../builtin_action_types/jira/service.test.ts | 918 ++++++++++-------- .../builtin_action_types/jira/service.ts | 66 +- .../lib/axios_utils.test.ts | 88 +- .../builtin_action_types/lib/axios_utils.ts | 68 ++ .../resilient/service.test.ts | 388 +++++--- .../builtin_action_types/resilient/service.ts | 32 +- .../swimlane/service.test.ts | 64 +- .../builtin_action_types/swimlane/service.ts | 13 +- .../server/jira_simulation.ts | 19 +- 9 files changed, 1053 insertions(+), 603 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 2300143925b1e..1254d86e99066 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -8,7 +8,7 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../lib/axios_utils'; +import { request, createAxiosResponse } from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; @@ -29,10 +29,10 @@ jest.mock('../lib/axios_utils', () => { }); axios.create = jest.fn(() => axios); -const requestMock = utils.request as jest.Mock; +const requestMock = request as jest.Mock; const configurationUtilities = actionsConfigMock.create(); -const issueTypesResponse = { +const issueTypesResponse = createAxiosResponse({ data: { projects: [ { @@ -49,9 +49,9 @@ const issueTypesResponse = { }, ], }, -}; +}); -const fieldsResponse = { +const fieldsResponse = createAxiosResponse({ data: { projects: [ { @@ -98,7 +98,7 @@ const fieldsResponse = { }, ], }, -}; +}); const issueResponse = { id: '10267', @@ -108,6 +108,31 @@ const issueResponse = { const issuesResponse = [issueResponse]; +const mockNewAPI = () => + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + }) + ); + +const mockOldAPI = () => + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + }) + ); + describe('Jira service', () => { let service: ExternalService; @@ -183,18 +208,34 @@ describe('Jira service', () => { }); describe('getIncident', () => { + const axiosRes = { + data: { + id: '1', + key: 'CK-1', + fields: { + summary: 'title', + description: 'description', + created: '2021-10-20T19:41:02.754+0300', + updated: '2021-10-20T19:41:02.754+0300', + }, + }, + }; + test('it returns the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, - })); + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); const res = await service.getIncident('1'); - expect(res).toEqual({ id: '1', key: 'CK-1', summary: 'title', description: 'description' }); + expect(res).toEqual({ + id: '1', + key: 'CK-1', + summary: 'title', + description: 'description', + created: '2021-10-20T19:41:02.754+0300', + updated: '2021-10-20T19:41:02.754+0300', + }); }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { id: '1', key: 'CK-1' }, - })); + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ @@ -215,9 +256,38 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get incident with id 1. Error: An error has occurred Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ ...axiosRes, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][Jira]: Unable to get incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][Jira]: Unable to get incident with id 1. Error: Response is missing at least one of the expected fields: id,key Reason: unknown: errorResponse was null' + ); + }); }); describe('createIncident', () => { + const incident = { + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + test('it creates the incident correctly', async () => { /* The response from Jira when creating an issue contains only the key and the id. The function makes the following calls when creating an issue: @@ -225,24 +295,19 @@ describe('Jira service', () => { 2. Create the issue. 3. Get the created issue with all the necessary fields. */ - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + }) + ); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + }) + ); - const res = await service.createIncident({ - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }); + const res = await service.createIncident(incident); expect(res).toEqual({ title: 'CK-1', @@ -260,24 +325,30 @@ describe('Jira service', () => { 3. Get the created issue with all the necessary fields. */ // getIssueType mocks - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, }, - }, - })); + }) + ); // getIssueType mocks requestMock.mockImplementationOnce(() => issueTypesResponse); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + }) + ); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + }) + ); const res = await service.createIncident({ incident: { @@ -317,25 +388,31 @@ describe('Jira service', () => { }); test('removes newline characters and trialing spaces from summary', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, }, - }, - })); + }) + ); // getIssueType mocks requestMock.mockImplementationOnce(() => issueTypesResponse); // getIssueType mocks - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { summary: 'test', description: 'description' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { summary: 'test', description: 'description' } }, + }) + ); - requestMock.mockImplementationOnce(() => ({ - data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + }) + ); await service.createIncident({ incident: { @@ -368,24 +445,17 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - fields: { created: '2020-04-27T10:59:46.202Z' }, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); - await service.createIncident({ - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: 'RJ-107', - }, - }); + await service.createIncident(incident); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -414,44 +484,55 @@ describe('Jira service', () => { throw error; }); - await expect( - service.createIncident({ - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }) - ).rejects.toThrow( + await expect(service.createIncident(incident)).rejects.toThrow( '[Action][Jira]: Unable to create incident. Error: An error has occurred. Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Jira]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Jira]: Unable to create incident. Error: Response is missing at least one of the expected fields: id. Reason: unknown: errorResponse was null' + ); + }); }); describe('updateIncident', () => { + const incident = { + incidentId: '1', + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + test('it updates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - fields: { updated: '2020-04-27T10:59:46.202Z' }, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); - const res = await service.updateIncident({ - incidentId: '1', - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }); + const res = await service.updateIncident(incident); expect(res).toEqual({ title: 'CK-1', @@ -462,25 +543,17 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - fields: { updated: '2020-04-27T10:59:46.202Z' }, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); - await service.updateIncident({ - incidentId: '1', - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: 'RJ-107', - }, - }); + await service.updateIncident(incident); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -509,41 +582,42 @@ describe('Jira service', () => { throw error; }); - await expect( - service.updateIncident({ - incidentId: '1', - incident: { - summary: 'title', - description: 'desc', - labels: [], - issueType: '10006', - priority: 'High', - parent: null, - }, - }) - ).rejects.toThrow( + await expect(service.updateIncident(incident)).rejects.toThrow( '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.updateIncident(incident)).rejects.toThrow( + '[Action][Jira]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('createComment', () => { + const commentReq = { + incidentId: '1', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; test('it creates the comment correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - created: '2020-04-27T10:59:46.202Z', - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + }) + ); - const res = await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + const res = await service.createComment(commentReq); expect(res).toEqual({ commentId: 'comment-1', @@ -553,21 +627,17 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - key: 'CK-1', - created: '2020-04-27T10:59:46.202Z', - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + }) + ); - await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + await service.createComment(commentReq); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -586,29 +656,33 @@ describe('Jira service', () => { throw error; }); - await expect( - service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }) - ).rejects.toThrow( + await expect(service.createComment(commentReq)).rejects.toThrow( '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Jira]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Jira]: Unable to create comment at incident with id 1. Error: Response is missing at least one of the expected fields: id,created. Reason: unknown: errorResponse was null' + ); + }); }); describe('getCapabilities', () => { test('it should return the capabilities', async () => { - requestMock.mockImplementation(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); const res = await service.getCapabilities(); expect(res).toEqual({ capabilities: { @@ -618,13 +692,7 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); await service.getCapabilities(); @@ -649,16 +717,34 @@ describe('Jira service', () => { ); }); - test('it should throw an auth error', async () => { + test('it should return unknown if the error is a string', async () => { requestMock.mockImplementation(() => { const error = new Error('An error has occurred'); - // @ts-ignore this can happen! + // @ts-ignore error.response = { data: 'Unauthorized' }; throw error; }); await expect(service.getCapabilities()).rejects.toThrow( - '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Unauthorized' + '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: unknown: errorResponse.errors was null' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: Response is missing at least one of the expected fields: capabilities. Reason: unknown: errorResponse was null' ); }); }); @@ -666,13 +752,7 @@ describe('Jira service', () => { describe('getIssueTypes', () => { describe('Old API', () => { test('it should return the issue types', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => issueTypesResponse); @@ -691,13 +771,7 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => issueTypesResponse); @@ -713,13 +787,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -731,25 +799,30 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockOldAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('New API', () => { test('it should return the issue types', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); - requestMock.mockImplementationOnce(() => ({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + }) + ); const res = await service.getIssueTypes(); @@ -766,22 +839,15 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); - requestMock.mockImplementationOnce(() => ({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - })); + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + }) + ); await service.getIssueTypes(); @@ -795,16 +861,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -816,19 +873,25 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockNewAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); }); describe('getFieldsByIssueType', () => { describe('Old API', () => { test('it should return the fields', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => fieldsResponse); @@ -857,13 +920,7 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementationOnce(() => fieldsResponse); @@ -879,13 +936,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', - }, - }, - })); + mockOldAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -897,43 +948,48 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockOldAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('New API', () => { test('it should return the fields', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); - - requestMock.mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { - required: false, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { + mockNewAPI(); + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { + required: false, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3', }, - ], - defaultValue: { - name: 'Medium', - id: '3', }, - }, - ], - }, - })); + ], + }, + }) + ); const res = await service.getFieldsByIssueType('10006'); @@ -954,39 +1010,32 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); - - requestMock.mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { - required: true, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { + mockNewAPI(); + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { + required: true, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3', }, - ], - defaultValue: { - name: 'Medium', - id: '3', }, - }, - ], - }, - })); + ], + }, + }) + ); await service.getFieldsByIssueType('10006'); @@ -1000,16 +1049,7 @@ describe('Jira service', () => { }); test('it should throw an error', async () => { - requestMock.mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', - }, - }, - })); + mockNewAPI(); requestMock.mockImplementation(() => { const error: ResponseError = new Error('An error has occurred'); @@ -1021,16 +1061,30 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + mockNewAPI(); + + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); }); describe('getIssues', () => { test('it should return the issues', async () => { - requestMock.mockImplementation(() => ({ - data: { - issues: issuesResponse, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + issues: issuesResponse, + }, + }) + ); const res = await service.getIssues('Test title'); @@ -1044,11 +1098,13 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - issues: issuesResponse, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + issues: issuesResponse, + }, + }) + ); await service.getIssues('Test title'); expect(requestMock).toHaveBeenLastCalledWith({ @@ -1071,13 +1127,25 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issues. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssues('Test title')).rejects.toThrow( + '[Action][Jira]: Unable to get issues. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('getIssue', () => { test('it should return a single issue', async () => { - requestMock.mockImplementation(() => ({ - data: issueResponse, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: issueResponse, + }) + ); const res = await service.getIssue('RJ-107'); @@ -1089,11 +1157,13 @@ describe('Jira service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - issues: issuesResponse, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + issues: issuesResponse, + }, + }) + ); await service.getIssue('RJ-107'); expect(requestMock).toHaveBeenLastCalledWith({ @@ -1116,81 +1186,105 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get issue with id RJ-107. Error: An error has occurred. Reason: Could not get issue types' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIssue('Test title')).rejects.toThrow( + '[Action][Jira]: Unable to get issue with id Test title. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown: errorResponse was null' + ); + }); }); describe('getFields', () => { const callMocks = () => { requestMock - .mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, }, - }, - })) - .mockImplementationOnce(() => ({ - data: { - values: issueTypesResponse.data.projects[0].issuetypes, - }, - })) - .mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, }, - }, - })) - .mockImplementationOnce(() => ({ - data: { - capabilities: { - 'list-project-issuetypes': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', - 'list-issuetype-fields': - 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, }, - }, - })) - .mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { required: true, schema: { type: 'string' }, fieldId: 'description' }, - { - required: false, - schema: { type: 'string' }, - fieldId: 'priority', - allowedValues: [ - { + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'description' }, + { + required: false, + schema: { type: 'string' }, + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3', }, - ], - defaultValue: { - name: 'Medium', - id: '3', }, - }, - ], - }, - })) - .mockImplementationOnce(() => ({ - data: { - values: [ - { required: true, schema: { type: 'string' }, fieldId: 'summary' }, - { required: true, schema: { type: 'string' }, fieldId: 'description' }, - ], - }, - })); + ], + }, + }) + ) + .mockImplementationOnce(() => + createAxiosResponse({ + data: { + values: [ + { required: true, schema: { type: 'string' }, fieldId: 'summary' }, + { required: true, schema: { type: 'string' }, fieldId: 'description' }, + ], + }, + }) + ); }; + beforeEach(() => { jest.resetAllMocks(); }); + test('it should call request with correct arguments', async () => { callMocks(); await service.getFields(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index be0240e705a65..a3262a526e2f4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -27,7 +27,7 @@ import { } from './types'; import * as i18n from './translations'; -import { request, getErrorMessage } from '../lib/axios_utils'; +import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; const VERSION = '2'; @@ -111,19 +111,15 @@ export const createExternalService = ( .filter((item) => !isEmpty(item)) .join(', '); - const createErrorMessage = (errorResponse: ResponseError | string | null | undefined): string => { + const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { if (errorResponse == null) { - return ''; - } - if (typeof errorResponse === 'string') { - // Jira error.response.data can be string!! - return errorResponse; + return 'unknown: errorResponse was null'; } const { errorMessages, errors } = errorResponse; if (errors == null) { - return ''; + return 'unknown: errorResponse.errors was null'; } if (Array.isArray(errorMessages) && errorMessages.length > 0) { @@ -185,9 +181,14 @@ export const createExternalService = ( configurationUtilities, }); - const { fields, ...rest } = res.data; + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'key'], + }); + + const { fields, id: incidentId, key } = res.data; - return { ...rest, ...fields }; + return { id: incidentId, key, created: fields.created, updated: fields.updated, ...fields }; } catch (error) { throw new Error( getErrorMessage( @@ -234,6 +235,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id'], + }); + const updatedIncident = await getIncident(res.data.id); return { @@ -266,7 +272,7 @@ export const createExternalService = ( const fields = createFields(projectKey, incidentWithoutNullValues); try { - await request({ + const res = await request({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, @@ -275,6 +281,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const updatedIncident = await getIncident(incidentId as string); return { @@ -309,6 +319,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'created'], + }); + return { commentId: comment.commentId, externalCommentId: res.data.id, @@ -336,6 +351,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['capabilities'], + }); + return { ...res.data }; } catch (error) { throw new Error( @@ -362,6 +382,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const issueTypes = res.data.projects[0]?.issuetypes ?? []; return normalizeIssueTypes(issueTypes); } else { @@ -373,6 +397,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const issueTypes = res.data.values; return normalizeIssueTypes(issueTypes); } @@ -401,6 +429,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; return normalizeFields(fields); } else { @@ -412,6 +444,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const fields = res.data.values.reduce( (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ ...acc, @@ -471,6 +507,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return normalizeSearchResults(res.data?.issues ?? []); } catch (error) { throw new Error( @@ -495,6 +535,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return normalizeIssue(res.data ?? {}); } catch (error) { throw new Error( diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 287f74c6bc703..d0177e0e5a8a2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -10,7 +10,14 @@ import { Agent as HttpsAgent } from 'https'; import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; -import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; +import { + addTimeZoneToDate, + request, + patch, + getErrorMessage, + throwIfResponseIsNotValid, + createAxiosResponse, +} from './axios_utils'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; import { getCustomAgents } from './get_custom_agents'; @@ -292,3 +299,82 @@ describe('getErrorMessage', () => { expect(msg).toBe('[Action][My connector name]: An error has occurred'); }); }); + +describe('throwIfResponseIsNotValid', () => { + const res = createAxiosResponse({ + headers: { ['content-type']: 'application/json' }, + data: { incident: { id: '1' } }, + }); + + test('it does NOT throw if the request is valid', () => { + expect(() => throwIfResponseIsNotValid({ res })).not.toThrow(); + }); + + test('it does throw if the content-type is not json', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, headers: { ['content-type']: 'text/html' } }, + }) + ).toThrow( + 'Unsupported content type: text/html in GET https://example.com. Supported content types: application/json' + ); + }); + + test('it does throw if the content-type is undefined', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, headers: {} }, + }) + ).toThrow( + 'Unsupported content type: undefined in GET https://example.com. Supported content types: application/json' + ); + }); + + test('it does throw if the data is not an object or array', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: 'string' }, + }) + ).toThrow('Response is not a valid JSON'); + }); + + test('it does NOT throw if the data is an array', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: ['test'] }, + }) + ).not.toThrow(); + }); + + test.each(['', [], {}])('it does NOT throw if the data is %p', (data) => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data }, + }) + ).not.toThrow(); + }); + + test('it does throw if the required attribute is not in the response', () => { + expect(() => + throwIfResponseIsNotValid({ res, requiredAttributesToBeInTheResponse: ['not-exist'] }) + ).toThrow('Response is missing at least one of the expected fields: not-exist'); + }); + + test('it does throw if the required attribute are defined and the data is an array', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: ['test'] }, + requiredAttributesToBeInTheResponse: ['not-exist'], + }) + ).toThrow('Response is missing at least one of the expected fields: not-exist'); + }); + + test('it does NOT throw if the value of the required attribute is null', () => { + expect(() => + throwIfResponseIsNotValid({ + res: { ...res, data: { id: null } }, + requiredAttributesToBeInTheResponse: ['id'], + }) + ).not.toThrow(); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index af353e1d1da5a..43c9d276e6574 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isObjectLike, isEmpty } from 'lodash'; import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios'; import { Logger } from '../../../../../../src/core/server'; import { getCustomAgents } from './get_custom_agents'; @@ -76,3 +77,70 @@ export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { export const getErrorMessage = (connector: string, msg: string) => { return `[Action][${connector}]: ${msg}`; }; + +export const throwIfResponseIsNotValid = ({ + res, + requiredAttributesToBeInTheResponse = [], +}: { + res: AxiosResponse; + requiredAttributesToBeInTheResponse?: string[]; +}) => { + const requiredContentType = 'application/json'; + const contentType = res.headers['content-type'] ?? 'undefined'; + const data = res.data; + + /** + * Check that the content-type of the response is application/json. + * Then includes is added because the header can be application/json;charset=UTF-8. + */ + if (!contentType.includes(requiredContentType)) { + throw new Error( + `Unsupported content type: ${contentType} in ${res.config.method} ${res.config.url}. Supported content types: ${requiredContentType}` + ); + } + + /** + * Check if the response is a JS object (data != null && typeof data === 'object') + * in case the content type is application/json but for some reason the response is not. + * Empty responses (204 No content) are ignored because the typeof data will be string and + * isObjectLike will fail. + * Axios converts automatically JSON to JS objects. + */ + if (!isEmpty(data) && !isObjectLike(data)) { + throw new Error('Response is not a valid JSON'); + } + + if (requiredAttributesToBeInTheResponse.length > 0) { + const requiredAttributesError = new Error( + `Response is missing at least one of the expected fields: ${requiredAttributesToBeInTheResponse.join( + ',' + )}` + ); + + /** + * If the response is an array and requiredAttributesToBeInTheResponse + * are not empty then we thrown an error assuming that the consumer + * expects an object response and not an array. + */ + + if (Array.isArray(data)) { + throw requiredAttributesError; + } + + requiredAttributesToBeInTheResponse.forEach((attr) => { + // Check only for undefined as null is a valid value + if (data[attr] === undefined) { + throw requiredAttributesError; + } + }); + } +}; + +export const createAxiosResponse = (res: Partial): AxiosResponse => ({ + data: {}, + status: 200, + statusText: 'OK', + headers: { ['content-type']: 'application/json' }, + config: { method: 'GET', url: 'https://example.com' }, + ...res, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index ba55543386225..094b8150850df 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -8,7 +8,7 @@ import axios from 'axios'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; -import * as utils from '../lib/axios_utils'; +import { request, createAxiosResponse } from '../lib/axios_utils'; import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; @@ -27,7 +27,7 @@ jest.mock('../lib/axios_utils', () => { }); axios.create = jest.fn(() => axios); -const requestMock = utils.request as jest.Mock; +const requestMock = request as jest.Mock; const now = Date.now; const TIMESTAMP = 1589391874472; const configurationUtilities = actionsConfigMock.create(); @@ -38,44 +38,50 @@ const configurationUtilities = actionsConfigMock.create(); // b) Update the incident // c) Get the updated incident const mockIncidentUpdate = (withUpdateError = false) => { - requestMock.mockImplementationOnce(() => ({ - data: { - id: '1', - name: 'title', - description: { - format: 'html', - content: 'description', + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + id: '1', + name: 'title', + description: { + format: 'html', + content: 'description', + }, + incident_type_ids: [1001, 16, 12], + severity_code: 6, }, - incident_type_ids: [1001, 16, 12], - severity_code: 6, - }, - })); + }) + ); if (withUpdateError) { requestMock.mockImplementationOnce(() => { throw new Error('An error has occurred'); }); } else { - requestMock.mockImplementationOnce(() => ({ + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + success: true, + id: '1', + inc_last_modified_date: 1589391874472, + }, + }) + ); + } + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ data: { - success: true, id: '1', + name: 'title_updated', + description: { + format: 'html', + content: 'desc_updated', + }, inc_last_modified_date: 1589391874472, }, - })); - } - - requestMock.mockImplementationOnce(() => ({ - data: { - id: '1', - name: 'title_updated', - description: { - format: 'html', - content: 'desc_updated', - }, - inc_last_modified_date: 1589391874472, - }, - })); + }) + ); }; describe('IBM Resilient service', () => { @@ -207,24 +213,28 @@ describe('IBM Resilient service', () => { describe('getIncident', () => { test('it returns the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - name: '1', - description: { - format: 'html', - content: 'description', + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + name: '1', + description: { + format: 'html', + content: 'description', + }, }, - }, - })); + }) + ); const res = await service.getIncident('1'); expect(res).toEqual({ id: '1', name: '1', description: 'description' }); }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { id: '1' }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { id: '1' }, + }) + ); await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ @@ -246,28 +256,42 @@ describe('IBM Resilient service', () => { 'Unable to get incident with id 1. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); describe('createIncident', () => { + const incident = { + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, + }; + test('it creates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - name: 'title', - description: 'description', - discovered_date: 1589391874472, - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + name: 'title', + description: 'description', + discovered_date: 1589391874472, + create_date: 1589391874472, + }, + }) + ); - const res = await service.createIncident({ - incident: { - name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 6, - }, - }); + const res = await service.createIncident(incident); expect(res).toEqual({ title: '1', @@ -278,24 +302,19 @@ describe('IBM Resilient service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - name: 'title', - description: 'description', - discovered_date: 1589391874472, - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + name: 'title', + description: 'description', + discovered_date: 1589391874472, + create_date: 1589391874472, + }, + }) + ); - await service.createIncident({ - incident: { - name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 6, - }, - }); + await service.createIncident(incident); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -334,20 +353,39 @@ describe('IBM Resilient service', () => { '[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to create incident. Error: Response is missing at least one of the expected fields: id,create_date.' + ); + }); }); describe('updateIncident', () => { + const req = { + incidentId: '1', + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, + }; test('it updates the incident correctly', async () => { mockIncidentUpdate(); - const res = await service.updateIncident({ - incidentId: '1', - incident: { - name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 6, - }, - }); + const res = await service.updateIncident(req); expect(res).toEqual({ title: '1', @@ -430,38 +468,59 @@ describe('IBM Resilient service', () => { test('it should throw an error', async () => { mockIncidentUpdate(true); - await expect( - service.updateIncident({ - incidentId: '1', - incident: { + await expect(service.updateIncident(req)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + // get incident request + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + id: '1', name: 'title', - description: 'desc', - incidentTypes: [1001], - severityCode: 5, + description: { + format: 'html', + content: 'description', + }, + incident_type_ids: [1001, 16, 12], + severity_code: 6, }, }) - ).rejects.toThrow( - '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred' + ); + + // update incident request + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.updateIncident(req)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json' ); }); }); describe('createComment', () => { + const req = { + incidentId: '1', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; + test('it creates the comment correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + create_date: 1589391874472, + }, + }) + ); - const res = await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + const res = await service.createComment(req); expect(res).toEqual({ commentId: 'comment-1', @@ -471,20 +530,16 @@ describe('IBM Resilient service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { - id: '1', - create_date: 1589391874472, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + create_date: 1589391874472, + }, + }) + ); - await service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }); + await service.createComment(req); expect(requestMock).toHaveBeenCalledWith({ axios, @@ -506,27 +561,31 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - await expect( - service.createComment({ - incidentId: '1', - comment: { - comment: 'comment', - commentId: 'comment-1', - }, - }) - ).rejects.toThrow( + await expect(service.createComment(req)).rejects.toThrow( '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createComment(req)).rejects.toThrow( + '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); describe('getIncidentTypes', () => { test('it creates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - values: incidentTypes, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + values: incidentTypes, + }, + }) + ); const res = await service.getIncidentTypes(); @@ -545,15 +604,27 @@ describe('IBM Resilient service', () => { '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIncidentTypes()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); describe('getSeverity', () => { test('it creates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - values: severity, - }, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + values: severity, + }, + }) + ); const res = await service.getSeverity(); @@ -578,17 +649,29 @@ describe('IBM Resilient service', () => { throw new Error('An error has occurred'); }); - await expect(service.getIncidentTypes()).rejects.toThrow( - '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' + await expect(service.getSeverity()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get severity. Error: An error has occurred.' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getSeverity()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get severity. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' ); }); }); describe('getFields', () => { test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: resilientFields, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: resilientFields, + }) + ); await service.getFields(); expect(requestMock).toHaveBeenCalledWith({ @@ -598,10 +681,13 @@ describe('IBM Resilient service', () => { url: 'https://resilient.elastic.co/rest/orgs/201/types/incident/fields', }); }); + test('it returns common fields correctly', async () => { - requestMock.mockImplementation(() => ({ - data: resilientFields, - })); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: resilientFields, + }) + ); const res = await service.getFields(); expect(res).toEqual(resilientFields); }); @@ -614,5 +700,15 @@ describe('IBM Resilient service', () => { 'Unable to get fields. Error: An error has occurred' ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getFields()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index 2f385315e4392..a469c631fac37 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -24,7 +24,7 @@ import { } from './types'; import * as i18n from './translations'; -import { getErrorMessage, request } from '../lib/axios_utils'; +import { getErrorMessage, request, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; const VIEW_INCIDENT_URL = `#incidents`; @@ -134,6 +134,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return { ...res.data, description: res.data.description?.content ?? '' }; } catch (error) { throw new Error( @@ -182,6 +186,11 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'create_date'], + }); + return { title: `${res.data.id}`, id: `${res.data.id}`, @@ -212,6 +221,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + if (!res.data.success) { throw new Error(res.data.message); } @@ -245,6 +258,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + return { commentId: comment.commentId, externalCommentId: res.data.id, @@ -270,6 +287,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const incidentTypes = res.data?.values ?? []; return incidentTypes.map((type: { value: string; label: string }) => ({ id: type.value, @@ -292,6 +313,10 @@ export const createExternalService = ( configurationUtilities, }); + throwIfResponseIsNotValid({ + res, + }); + const incidentTypes = res.data?.values ?? []; return incidentTypes.map((type: { value: string; label: string }) => ({ id: type.value, @@ -312,6 +337,11 @@ export const createExternalService = ( logger, configurationUtilities, }); + + throwIfResponseIsNotValid({ + res, + }); + return res.data ?? []; } catch (error) { throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}.`)); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 7b3f310a99e0e..21bc4894c5717 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -10,7 +10,7 @@ import axios from 'axios'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { Logger } from '../../../../../../src/core/server'; import { actionsConfigMock } from '../../actions_config.mock'; -import * as utils from '../lib/axios_utils'; +import { request, createAxiosResponse } from '../lib/axios_utils'; import { createExternalService } from './service'; import { mappings } from './mocks'; import { ExternalService } from './types'; @@ -27,7 +27,7 @@ jest.mock('../lib/axios_utils', () => { }); axios.create = jest.fn(() => axios); -const requestMock = utils.request as jest.Mock; +const requestMock = request as jest.Mock; const configurationUtilities = actionsConfigMock.create(); describe('Swimlane Service', () => { @@ -152,9 +152,7 @@ describe('Swimlane Service', () => { }; test('it creates a record correctly', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); const res = await service.createRecord({ incident, @@ -169,9 +167,7 @@ describe('Swimlane Service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); await service.createRecord({ incident, @@ -207,6 +203,24 @@ describe('Swimlane Service', () => { `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createRecord({ incident })).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown` + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createRecord({ incident })).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: Response is missing at least one of the expected fields: id,name,createdDate. Reason: unknown` + ); + }); }); describe('updateRecord', () => { @@ -218,9 +232,7 @@ describe('Swimlane Service', () => { const incidentId = '123'; test('it updates a record correctly', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); const res = await service.updateRecord({ incident, @@ -236,9 +248,7 @@ describe('Swimlane Service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); await service.updateRecord({ incident, @@ -276,6 +286,24 @@ describe('Swimlane Service', () => { `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json. Reason: unknown` + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: Response is missing at least one of the expected fields: id,name,modifiedDate. Reason: unknown` + ); + }); }); describe('createComment', () => { @@ -289,9 +317,7 @@ describe('Swimlane Service', () => { const createdDate = '2021-06-01T17:29:51.092Z'; test('it updates a record correctly', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); const res = await service.createComment({ comment, @@ -306,9 +332,7 @@ describe('Swimlane Service', () => { }); test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data, - })); + requestMock.mockImplementation(() => createAxiosResponse({ data })); await service.createComment({ comment, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index f68d22121dbcc..d917d7f5677bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -9,7 +9,7 @@ import { Logger } from '@kbn/logging'; import axios from 'axios'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { getErrorMessage, request } from '../lib/axios_utils'; +import { getErrorMessage, request, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { getBodyForEventAction } from './helpers'; import { CreateCommentParams, @@ -89,6 +89,12 @@ export const createExternalService = ( method: 'post', url: getPostRecordUrl(appId), }); + + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'name', 'createdDate'], + }); + return { id: res.data.id, title: res.data.name, @@ -124,6 +130,11 @@ export const createExternalService = ( url: getPostRecordIdUrl(appId, params.incidentId), }); + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'name', 'modifiedDate'], + }); + return { id: res.data.id, title: res.data.name, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts index 26a9c1bcadf6e..e9e6d6732327a 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts @@ -30,7 +30,6 @@ export function initPlugin(router: IRouter, path: string) { return jsonResponse(res, 200, { id: '123', key: 'CK-1', - created: '2020-04-27T14:17:45.490Z', }); } ); @@ -48,12 +47,7 @@ export function initPlugin(router: IRouter, path: string) { req: KibanaRequest, res: KibanaResponseFactory ): Promise> { - return jsonResponse(res, 200, { - id: '123', - key: 'CK-1', - created: '2020-04-27T14:17:45.490Z', - updated: '2020-04-27T14:17:45.490Z', - }); + return jsonResponse(res, 204, {}); } ); @@ -73,10 +67,12 @@ export function initPlugin(router: IRouter, path: string) { return jsonResponse(res, 200, { id: '123', key: 'CK-1', - created: '2020-04-27T14:17:45.490Z', - updated: '2020-04-27T14:17:45.490Z', - summary: 'title', - description: 'description', + fields: { + created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', + summary: 'title', + description: 'description', + }, }); } ); @@ -97,6 +93,7 @@ export function initPlugin(router: IRouter, path: string) { return jsonResponse(res, 200, { id: '123', created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', }); } ); From 262d0cdafd9a2a85aab989ee9c7663bc74e597a9 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 25 Oct 2021 12:07:50 +0300 Subject: [PATCH 09/41] [TSVB] Error when selecting * for override index_pattern field on the string mode (#114450) * Add condition so that preselect first timefield from the list if we use * as index pattern * Add default timefield for string index pattern mode * Add comment for condition * Fix lint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/vis_data/get_interval_and_timefield.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts index b7a22abd825e0..7c17f003dfbab 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts @@ -24,10 +24,15 @@ export function getIntervalAndTimefield( { min, max, maxBuckets }: IntervalParams, series?: Series ) { - const timeField = + let timeField = (series?.override_index_pattern ? series.series_time_field : panel.time_field) || index.indexPattern?.timeFieldName; + // should use @timestamp as default timeField for es indeces if user doesn't provide timeField + if (!panel.use_kibana_indexes && !timeField) { + timeField = '@timestamp'; + } + if (panel.use_kibana_indexes) { validateField(timeField!, index); } From 6b5b06fc038c438ea850752d7255036913f4ebf7 Mon Sep 17 00:00:00 2001 From: Giorgos Bamparopoulos Date: Mon, 25 Oct 2021 10:24:00 +0100 Subject: [PATCH 10/41] Update APM linting dev doc with instructions about how to install the pre-commit hook and a link to the Kibana guide (#115924) --- x-pack/plugins/apm/dev_docs/linting.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugins/apm/dev_docs/linting.md b/x-pack/plugins/apm/dev_docs/linting.md index edf3e813a88e9..3dbd7b5b27484 100644 --- a/x-pack/plugins/apm/dev_docs/linting.md +++ b/x-pack/plugins/apm/dev_docs/linting.md @@ -19,3 +19,12 @@ yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write ``` node scripts/eslint.js x-pack/plugins/apm ``` + +## Install pre-commit hook (optional) +In case you want to run a couple of checks like linting or check the file casing of the files to commit, we provide a way to install a pre-commit hook. To configure it you just need to run the following: + +`node scripts/register_git_hook` + +After the script completes the pre-commit hook will be created within the file .git/hooks/pre-commit. If you choose to not install it, don’t worry, we still run a quick CI check to provide feedback earliest as we can about the same checks. + +More information about linting can be found in the [Kibana Guide](https://www.elastic.co/guide/en/kibana/current/kibana-linting.html). \ No newline at end of file From d66abdf25ede2d096a811f9179717366c1b934f5 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 25 Oct 2021 04:39:43 -0500 Subject: [PATCH 11/41] Fix bug with popup not closing (#115954) --- .../application/pages/logstash/pipeline.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx index 1f56ea22839e2..cf9b5628222f4 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx @@ -46,7 +46,7 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => cluster_uuid: clusterUuid, }) as any; const [data, setData] = useState({} as any); - const [detailVertexId, setDetailVertexId] = useState(null); + const [detailVertexId, setDetailVertexId] = useState(undefined); const { updateTotalItemCount } = useTable('logstash.pipelines'); const title = i18n.translate('xpack.monitoring.logstash.pipeline.routeTitle', { @@ -128,18 +128,19 @@ export const LogStashPipelinePage: React.FC = ({ clusters }) => const timeseriesTooltipXValueFormatter = (xValue: any) => moment(xValue).format(dateFormat); const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); - const onVertexChange = useCallback( - (vertex: any) => { - if (!vertex) { - setDetailVertexId(null); - } else { - setDetailVertexId(vertex.id); - } + const onVertexChange = useCallback((vertex: any) => { + if (!vertex) { + setDetailVertexId(null); + } else { + setDetailVertexId(vertex.id); + } + }, []); + useEffect(() => { + if (detailVertexId !== undefined) { getPageData(); - }, - [getPageData] - ); + } + }, [detailVertexId, getPageData]); const onChangePipelineHash = useCallback(() => { window.location.hash = getSafeForExternalLink( From ed99e2466adc0e8f97ab79abe134200d0ad8c44b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 25 Oct 2021 11:50:35 +0200 Subject: [PATCH 12/41] [ML] Fix legend text colors for Vega based charts in dark mode. (#115911) Fixes the legend text colors for Vega based charts in dark mode. --- .../scatterplot_matrix_vega_lite_spec.ts | 5 +++++ .../classification_exploration/evaluate_panel.tsx | 6 +++++- .../get_roc_curve_chart_vega_lite_spec.tsx | 14 +++++++++----- .../classification_creation.ts | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index 7291f7bbfa838..861b3727cea1b 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -107,6 +107,11 @@ export const getScatterplotMatrixVegaLiteSpec = ( labelColor: euiTheme.euiTextSubduedColor, titleColor: euiTheme.euiTextSubduedColor, }, + legend: { + orient: 'right', + labelColor: euiTheme.euiTextSubduedColor, + titleColor: euiTheme.euiTextSubduedColor, + }, }, repeat: { column: vegaColumns, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index fb103886635a9..00a63bcf2a414 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -26,6 +26,7 @@ import { useMlKibana } from '../../../../../contexts/kibana'; // Separate imports for lazy loadable VegaChart and related code import { VegaChart } from '../../../../../components/vega_chart'; import { VegaChartLoading } from '../../../../../components/vega_chart/vega_chart_loading'; +import { useCurrentEuiTheme } from '../../../../../components/color_range_legend'; import { ErrorCallout } from '../error_callout'; import { getDependentVar, DataFrameAnalyticsConfig } from '../../../../common'; @@ -33,6 +34,7 @@ import { DataFrameTaskStateType } from '../../../analytics_management/components import { ResultsSearchQuery } from '../../../../common/analytics'; import { ExpandableSection, HEADER_ITEMS_LOADING } from '../expandable_section'; + import { EvaluateStat } from './evaluate_stat'; import { EvaluationQualityMetricsTable } from './evaluation_quality_metrics_table'; @@ -107,6 +109,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se const { services: { docLinks }, } = useMlKibana(); + const { euiTheme } = useCurrentEuiTheme(); const [columns, setColumns] = useState([]); const [columnsData, setColumnsData] = useState([]); @@ -469,7 +472,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se vegaSpec={getRocCurveChartVegaLiteSpec( classificationClasses, rocCurveData, - getDependentVar(jobConfig.analysis) + getDependentVar(jobConfig.analysis), + euiTheme )} /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx index e9a6925476b02..ef5bcb83e871f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx @@ -10,6 +10,7 @@ import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; import { euiPaletteColorBlind, euiPaletteGray } from '@elastic/eui'; +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; @@ -43,7 +44,8 @@ export interface RocCurveDataRow extends RocCurveItem { export const getRocCurveChartVegaLiteSpec = ( classificationClasses: string[], data: RocCurveDataRow[], - legendTitle: string + legendTitle: string, + euiTheme: typeof euiThemeLight ): TopLevelSpec => { // we append two rows which make up the data for the diagonal baseline data.push({ tpr: 0, fpr: 0, threshold: 1, class_name: BASELINE }); @@ -59,6 +61,8 @@ export const getRocCurveChartVegaLiteSpec = ( config: { legend: { orient: 'right', + labelColor: euiTheme.euiTextSubduedColor, + titleColor: euiTheme.euiTextSubduedColor, }, view: { continuousHeight: SIZE, @@ -101,9 +105,9 @@ export const getRocCurveChartVegaLiteSpec = ( type: 'quantitative', axis: { tickColor: GRAY, - labelColor: GRAY, + labelColor: euiTheme.euiTextSubduedColor, domainColor: GRAY, - titleColor: GRAY, + titleColor: euiTheme.euiTextSubduedColor, }, }, y: { @@ -114,9 +118,9 @@ export const getRocCurveChartVegaLiteSpec = ( type: 'quantitative', axis: { tickColor: GRAY, - labelColor: GRAY, + labelColor: euiTheme.euiTextSubduedColor, domainColor: GRAY, - titleColor: GRAY, + titleColor: euiTheme.euiTextSubduedColor, }, }, tooltip: [ diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 7e07ac82c9dcf..a494b401480d6 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -53,7 +53,7 @@ export default function ({ getService }: FtrProviderContext) { // tick/grid/axis { color: '#DDDDDD', percentage: 50 }, // line - { color: '#98A2B3', percentage: 30 }, + { color: '#98A2B3', percentage: 10 }, ], scatterplotMatrixColorStats: [ // marker colors From 780c43513ad49c73515ec65a725df6a7a89eb445 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 25 Oct 2021 11:16:13 +0100 Subject: [PATCH 13/41] [Security Solution] Fix tooltip for timeline sourcerer (#115950) * fix tooltip for event picker * remove unused mock * remove unused dependency Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/sourcerer/index.test.tsx | 34 ++++- .../common/components/sourcerer/index.tsx | 103 +++++++------ .../search_or_filter/pick_events.test.tsx | 55 ++++++- .../timeline/search_or_filter/pick_events.tsx | 137 ++++++++++-------- 4 files changed, 215 insertions(+), 114 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index 352fc95447822..591cba6b19381 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -7,6 +7,9 @@ import React from 'react'; import { mount } from 'enzyme'; +import { waitFor } from '@testing-library/react'; + +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { Sourcerer } from './index'; import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; @@ -19,8 +22,6 @@ import { TestProviders, } from '../../mock'; import { createStore, State } from '../../store'; -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { waitFor } from '@testing-library/react'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -46,6 +47,7 @@ const mockOptions = [ const defaultProps = { scope: sourcererModel.SourcererScopeName.default, }; + describe('Sourcerer component', () => { beforeEach(() => { jest.clearAllMocks(); @@ -59,6 +61,34 @@ describe('Sourcerer component', () => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); + it('renders tooltip', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="sourcerer-tooltip"]').prop('content')).toEqual( + mockOptions + .map((p) => p.label) + .sort() + .join(', ') + ); + }); + + it('renders popover button inside tooltip', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="sourcerer-tooltip"] [data-test-subj="sourcerer-trigger"]') + .exists() + ).toBeTruthy(); + }); + // Using props callback instead of simulating clicks, // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects it('Mounts with all options selected', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 8d0bb6410651c..3e922176f9982 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -169,55 +169,62 @@ export const Sourcerer = React.memo(({ scope: scopeId } [isPopoverOpen, sourcererScope.selectedPatterns] ); + const buttonWithTooptip = useMemo(() => { + return tooltipContent ? ( + + {trigger} + + ) : ( + trigger + ); + }, [trigger, tooltipContent]); + return ( - - - - - <>{i18n.SELECT_INDEX_PATTERNS} - - - {i18n.INDEX_PATTERNS_SELECTION_LABEL} - - {comboBox} - - - - - {i18n.INDEX_PATTERNS_RESET} - - - - - {i18n.SAVE_INDEX_PATTERNS} - - - - - - + + + + <>{i18n.SELECT_INDEX_PATTERNS} + + + {i18n.INDEX_PATTERNS_SELECTION_LABEL} + + {comboBox} + + + + + {i18n.INDEX_PATTERNS_RESET} + + + + + {i18n.SAVE_INDEX_PATTERNS} + + + + + ); }); Sourcerer.displayName = 'Sourcerer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx index 3c6dc68edefcc..e519cfcd204a7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, within } from '@testing-library/react'; +import { EuiToolTip } from '@elastic/eui'; + import React from 'react'; import { PickEventType } from './pick_events'; import { @@ -19,6 +21,14 @@ import { TimelineEventsType } from '../../../../../common'; import { createStore } from '../../../../common/store'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +jest.mock('@elastic/eui', () => { + const actual = jest.requireActual('@elastic/eui'); + return { + ...actual, + EuiToolTip: jest.fn(), + }; +}); + describe('pick_events', () => { const defaultProps = { eventType: 'all' as TimelineEventsType, @@ -53,6 +63,23 @@ describe('pick_events', () => { }, }; const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + const mockTooltip = ({ + tooltipContent, + children, + }: { + tooltipContent: string; + children: React.ReactElement; + }) => ( +
+ {tooltipContent} + {children} +
+ ); + + beforeAll(() => { + (EuiToolTip as unknown as jest.Mock).mockImplementation(mockTooltip); + }); beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); @@ -68,6 +95,32 @@ describe('pick_events', () => { initialPatterns.sort().join('') ); }); + + it('renders tooltip', () => { + render( + + + + ); + + expect((EuiToolTip as unknown as jest.Mock).mock.calls[0][0].content).toEqual( + initialPatterns + .filter((p) => p != null) + .sort() + .join(', ') + ); + }); + + it('renders popover button inside tooltip', () => { + const wrapper = render( + + + + ); + const tooltip = wrapper.getByTestId('timeline-sourcerer-tooltip'); + expect(within(tooltip).getByTestId('sourcerer-timeline-trigger')).toBeTruthy(); + }); + it('correctly filters options', () => { const wrapper = render( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx index dbe04eccac521..6d86d7c0f1330 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -295,6 +295,20 @@ const PickEventTypeComponents: React.FC = ({ [isPopoverOpen, sourcererScope.selectedPatterns] ); + const buttonWithTooptip = useMemo(() => { + return tooltipContent ? ( + + {button} + + ) : ( + button + ); + }, [button, tooltipContent]); + const ButtonContent = useMemo( () => ( @@ -326,69 +340,66 @@ const PickEventTypeComponents: React.FC = ({ return ( - - - - - <>{i18n.SELECT_INDEX_PATTERNS} - - - {filter} - - - <> - - {comboBox} - - - {!showAdvanceSettings && ( - <> - - - {i18n.CONFIGURE_INDEX_PATTERNS} - - - )} - - - - - {i18n.DATA_SOURCES_RESET} - - - - - {i18n.SAVE_INDEX_PATTERNS} - - - - - - + + + + <>{i18n.SELECT_INDEX_PATTERNS} + + + {filter} + + + <> + + {comboBox} + + + {!showAdvanceSettings && ( + <> + + + {i18n.CONFIGURE_INDEX_PATTERNS} + + + )} + + + + + {i18n.DATA_SOURCES_RESET} + + + + + {i18n.SAVE_INDEX_PATTERNS} + + + + + ); }; From 8f75a715f566c2b7fc1e2be490dbedb2102dc8bf Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:26:45 +0300 Subject: [PATCH 14/41] [Discover] Add clock icon to time field column (#114996) * [Discover] Add clock icon to time field column * [Discover] fix linting * [Discover] fix functional tests * [Discover] fix functional test * [Discover] apply suggestion * [Discover] apply wording suggestion * [Discover] fix i18n * [Discover] update unit tests * [Discover] add custom label for classic table * [Discover] color icon black * [Discover] fix unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../doc_table/components/_table_header.scss | 3 ++- .../__snapshots__/table_header.test.tsx.snap | 11 +++++++- .../components/table_header/helpers.tsx | 2 +- .../components/table_header/table_header.tsx | 2 ++ .../table_header/table_header_column.tsx | 24 ++++++++++++++++-- .../discover_grid_columns.test.tsx | 10 +++++++- .../discover_grid/discover_grid_columns.tsx | 25 ++++++++++++++----- .../apps/context/_discover_navigation.ts | 2 +- test/functional/apps/discover/_data_grid.ts | 10 ++++---- .../apps/discover/_data_grid_context.ts | 2 +- .../apps/discover/_data_grid_field_data.ts | 4 +-- test/functional/apps/discover/_field_data.ts | 4 +-- .../discover/_field_data_with_fields_api.ts | 4 +-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 15 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss index 3450084e19269..3ea6fb5502764 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss @@ -1,7 +1,8 @@ .kbnDocTableHeader { white-space: nowrap; } -.kbnDocTableHeader button { +.kbnDocTableHeader button, +.kbnDocTableHeader svg { margin-left: $euiSizeXS * .5; } .kbnDocTableHeader__move, diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap index 18b0ae8699e3e..3f72349f3e2a0 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap @@ -15,7 +15,16 @@ exports[`TableHeader with time column renders correctly 1`] = ` class="kbnDocTableHeader__actions" data-test-subj="docTableHeader-time" > - Time + time + + + diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx index d313e95c1ebb1..f04454d33e9f2 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx @@ -28,7 +28,7 @@ export interface ColumnProps { export function getTimeColumn(timeFieldName: string): ColumnProps { return { name: timeFieldName, - displayName: 'Time', + displayName: timeFieldName, isSortable: true, isRemoveable: false, colLeftIdx: -1, diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx index f891e809ee702..1877c014ddcbd 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx @@ -45,6 +45,8 @@ export function TableHeader({ void; onMoveColumn?: (name: string, idx: number) => void; @@ -54,6 +56,8 @@ export function TableHeaderColumn({ displayName, isRemoveable, isSortable, + isTimeColumn, + customLabel, name, onChangeSortOrder, onMoveColumn, @@ -65,6 +69,14 @@ export function TableHeaderColumn({ const curColSort = sortOrder.find((pair) => pair[0] === name); const curColSortDir = (curColSort && curColSort[1]) || ''; + const timeAriaLabel = i18n.translate( + 'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel', + { defaultMessage: 'Primary time field.' } + ); + const timeTooltip = i18n.translate('discover.docTable.tableHeader.timeFieldIconTooltip', { + defaultMessage: 'This field represents the time that events occurred.', + }); + // If this is the _score column, and _score is not one of the columns inside the sort, show a // warning that the _score will not be retrieved from Elasticsearch const showScoreSortWarning = name === '_score' && !curColSort; @@ -183,7 +195,15 @@ export function TableHeaderColumn({ {showScoreSortWarning && } - {displayName} + {customLabel ?? displayName} + {isTimeColumn && ( + + )} {buttons .filter((button) => button.active) .map((button, idx) => ( diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx index 46e30dd23525b..e5ea657032403 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -117,7 +117,15 @@ describe('Discover grid columns ', function () { [Function], [Function], ], - "display": "Time (timestamp)", + "display": + timestamp + + + , "id": "timestamp", "initialWidth": 190, "isSortable": true, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx index 2f4c0b5167df8..5eb55a8e99cde 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDataGridColumn, EuiScreenReaderOnly } from '@elastic/eui'; +import { EuiDataGridColumn, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridSettings } from './types'; import type { IndexPattern } from '../../../../../data/common'; @@ -57,9 +57,6 @@ export function buildEuiGridColumn( defaultColumns: boolean, isSortEnabled: boolean ) { - const timeString = i18n.translate('discover.timeLabel', { - defaultMessage: 'Time', - }); const indexPatternField = indexPattern.getFieldByName(columnName); const column: EuiDataGridColumn = { id: columnName, @@ -88,7 +85,23 @@ export function buildEuiGridColumn( }; if (column.id === indexPattern.timeFieldName) { - column.display = `${timeString} (${indexPattern.timeFieldName})`; + const primaryTimeAriaLabel = i18n.translate( + 'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel', + { defaultMessage: 'Primary time field.' } + ); + const primaryTimeTooltip = i18n.translate( + 'discover.docTable.tableHeader.timeFieldIconTooltip', + { + defaultMessage: 'This field represents the time that events occurred.', + } + ); + + column.display = ( + + {indexPatternField?.customLabel ?? indexPattern.timeFieldName}{' '} + + + ); column.initialWidth = defaultTimeColumnWidth; } if (columnWidth > 0) { diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts index 1b8300f3345b1..60745bd64b8be 100644 --- a/test/functional/apps/context/_discover_navigation.ts +++ b/test/functional/apps/context/_discover_navigation.ts @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should open the context view with the same columns', async () => { const columnNames = await docTable.getHeaderFields(); - expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]); + expect(columnNames).to.eql(['@timestamp', ...TEST_COLUMN_NAMES]); }); it('should open the context view with the filters disabled', async () => { diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts index 4a343fb30384e..198691f3b8477 100644 --- a/test/functional/apps/discover/_data_grid.ts +++ b/test/functional/apps/discover/_data_grid.ts @@ -39,19 +39,19 @@ export default function ({ const getTitles = async () => (await testSubjects.getVisibleText('dataGridHeader')).replace(/\s|\r?\n|\r/g, ' '); - expect(await getTitles()).to.be('Time (@timestamp) Document'); + expect(await getTitles()).to.be('@timestamp Document'); await PageObjects.discover.clickFieldListItemAdd('bytes'); - expect(await getTitles()).to.be('Time (@timestamp) bytes'); + expect(await getTitles()).to.be('@timestamp bytes'); await PageObjects.discover.clickFieldListItemAdd('agent'); - expect(await getTitles()).to.be('Time (@timestamp) bytes agent'); + expect(await getTitles()).to.be('@timestamp bytes agent'); await PageObjects.discover.clickFieldListItemRemove('bytes'); - expect(await getTitles()).to.be('Time (@timestamp) agent'); + expect(await getTitles()).to.be('@timestamp agent'); await PageObjects.discover.clickFieldListItemRemove('agent'); - expect(await getTitles()).to.be('Time (@timestamp) Document'); + expect(await getTitles()).to.be('@timestamp Document'); }); }); } diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 3d9e01e1dee19..d12ada2070cff 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should open the context view with the same columns', async () => { const columnNames = await dataGrid.getHeaderFields(); - expect(columnNames).to.eql(['Time (@timestamp)', ...TEST_COLUMN_NAMES]); + expect(columnNames).to.eql(['@timestamp', ...TEST_COLUMN_NAMES]); }); it('should open the context view with the filters disabled', async () => { diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 94e8e942f86ba..91c2d5914732d 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -59,8 +59,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('doc view should show Time and _source columns', async function () { - const expectedHeader = 'Time (@timestamp) Document'; + it('doc view should show @timestamp and _source columns', async function () { + const expectedHeader = '@timestamp Document'; const DocHeader = await dataGrid.getHeaderFields(); expect(DocHeader.join(' ')).to.be(expectedHeader); }); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index 27407e9a0bc4d..28f147eeab55f 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -107,8 +107,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async function () { await kibanaServer.uiSettings.replace({}); }); - it('doc view should show Time and _source columns', async function () { - const expectedHeader = 'Time\n_source'; + it('doc view should show @timestamp and _source columns', async function () { + const expectedHeader = '@timestamp\n_source'; const docHeader = await find.byCssSelector('thead > tr:nth-child(1)'); const docHeaderText = await docHeader.getVisibleText(); expect(docHeaderText).to.be(expectedHeader); diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 666377ae7f794..f0dedb155fc9b 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -64,9 +64,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('doc view should show Time and Document columns', async function () { + it('doc view should show @timestamp and Document columns', async function () { const Docheader = await PageObjects.discover.getDocHeader(); - expect(Docheader).to.contain('Time'); + expect(Docheader).to.contain('@timestamp'); expect(Docheader).to.contain('Document'); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9e4249986c6a9..7291e94615477 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2532,7 +2532,6 @@ "discover.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", "discover.sourceViewer.errorMessageTitle": "エラーが発生しました", "discover.sourceViewer.refresh": "更新", - "discover.timeLabel": "時間", "discover.toggleSidebarAriaLabel": "サイドバーを切り替える", "discover.topNav.openOptionsPopover.description": "お知らせDiscoverでは、データの並べ替え、列のドラッグアンドドロップ、ドキュメントの比較を行う方法が改善されました。詳細設定で[クラシックテーブルを使用]を切り替えて、開始します。", "discover.topNav.openSearchPanel.manageSearchesButtonLabel": "検索の管理", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index db6d19d7550a9..2aa9cabb7866b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2557,7 +2557,6 @@ "discover.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。", "discover.sourceViewer.errorMessageTitle": "发生错误", "discover.sourceViewer.refresh": "刷新", - "discover.timeLabel": "时间", "discover.toggleSidebarAriaLabel": "切换侧边栏", "discover.topNav.openOptionsPopover.description": "好消息!Discover 有更好的方法排序数据、拖放列和比较文档。在“高级模式”中切换“使用经典表”来开始。", "discover.topNav.openSearchPanel.manageSearchesButtonLabel": "管理搜索", From 811724b003f058b88dd85fed45ce853fdc682b04 Mon Sep 17 00:00:00 2001 From: Orhan Toy Date: Mon, 25 Oct 2021 14:00:13 +0200 Subject: [PATCH 15/41] [App Search, Crawler] Link-ify domain name in table (#115735) --- .../crawler/components/domains_table.test.tsx | 12 ++++++++++++ .../components/crawler/components/domains_table.tsx | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx index 78049d5832e9a..76622f9c12822 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx @@ -87,6 +87,18 @@ describe('DomainsTable', () => { expect(tableContent).toContain('elastic.co'); }); + it('renders a clickable domain url', () => { + const basicTable = wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive(); + const link = basicTable.find('[data-test-subj="CrawlerDomainURL"]').at(0); + + expect(link.dive().text()).toContain('elastic.co'); + expect(link.props()).toEqual( + expect.objectContaining({ + to: '/engines/some-engine/crawler/domains/1234', + }) + ); + }); + it('renders a last crawled column', () => { expect(tableContent).toContain('Last activity'); expect(tableContent).toContain('Jan 1, 2020'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx index 7214eace25e2d..1f0f6be22102f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx @@ -17,6 +17,7 @@ import { FormattedNumber } from '@kbn/i18n/react'; import { DELETE_BUTTON_LABEL, MANAGE_BUTTON_LABEL } from '../../../../shared/constants'; import { KibanaLogic } from '../../../../shared/kibana'; +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../routes'; import { generateEnginePath } from '../../engine'; @@ -46,6 +47,14 @@ export const DomainsTable: React.FC = () => { defaultMessage: 'Domain URL', } ), + render: (_, domain: CrawlerDomain) => ( + + {domain.url} + + ), }, { field: 'lastCrawl', From 5d73e8c3f4265aed99eb349160d4157ff2a8601a Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 25 Oct 2021 14:26:12 +0200 Subject: [PATCH 16/41] [Lens] Opening advanced Intervals editor should not throw an error (#115801) * :bug: Fix issue with the first rendering * :white_check_mark: Add tests for valid and broken scenario Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../definitions/ranges/ranges.test.tsx | 48 ++++++++++++++++++- .../operations/definitions/ranges/ranges.tsx | 2 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index b85652481d5b2..2877094ac81f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -52,7 +52,7 @@ jest.mock('lodash', () => { const dataPluginMockValue = dataPluginMock.createStartContract(); // need to overwrite the formatter field first -dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ params }) => { +dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ id, params }) => { return { convert: ({ gte, lt }: { gte: string; lt: string }) => { if (params?.id === 'custom') { @@ -61,6 +61,9 @@ dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ p if (params?.id === 'bytes') { return `Bytes format: ${gte} - ${lt}`; } + if (!id) { + return 'Error'; + } return `${gte} - ${lt}`; }, }; @@ -476,6 +479,49 @@ describe('ranges', () => { expect(instance.find(DragDropBuckets).children).toHaveLength(1); }); + it('should use the parentFormat to create the trigger label', () => { + const updateLayerSpy = jest.fn(); + + const instance = mount( + + ); + + expect( + instance.find('[data-test-subj="indexPattern-ranges-popover-trigger"]').first().text() + ).toBe('0 - 1000'); + }); + + it('should not print error if the parentFormat is not provided', () => { + // while in the actual React implementation will print an error, here + // we intercept the formatter without an id assigned an print "Error" + const updateLayerSpy = jest.fn(); + + const instance = mount( + + ); + + expect( + instance.find('[data-test-subj="indexPattern-ranges-popover-trigger"]').first().text() + ).not.toBe('Error'); + }); + it('should add a new range', () => { const updateLayerSpy = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 29e7de18ca4ad..6e397a926c7a0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -196,7 +196,7 @@ export const rangeOperation: OperationDefinition Date: Mon, 25 Oct 2021 14:27:05 +0200 Subject: [PATCH 17/41] [TSVB] Show the loading spinner while loading in dashboard (#114244) * :bug: Fix metric rescale * :camera_flash: Restored old snapshots * :bug: Extend the fix to all scenarios * :camera_flash: Refresh snapshots for new fix * :lipstick: Add suspense with loading spinner while waiting for the component to be loaded * :bug: Move the styling one level deeper to avoid loading gaps * :bug: show loader since the beginning and speed up a bit embeddable load * :lipstick: Show the loader while waiting for the palette * :zap: Remove one chunk from TSVB async series * :bug: Restore previous async chunk * :ok_hand: Integrate feedback * :rotating_light: Fix linting issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/timeseries_visualization.tsx | 37 ++++++---- .../public/timeseries_vis_renderer.tsx | 69 +++++++++++-------- ...embeddable.ts => visualize_embeddable.tsx} | 10 +++ .../embeddable/visualize_embeddable_async.ts | 8 ++- 4 files changed, 79 insertions(+), 45 deletions(-) rename src/plugins/visualizations/public/embeddable/{visualize_embeddable.ts => visualize_embeddable.tsx} (98%) diff --git a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx index a73f9c6a5e092..886b569671a6b 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx @@ -8,8 +8,8 @@ import './timeseries_visualization.scss'; -import React, { useCallback, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { Suspense, useCallback, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart } from '@elastic/eui'; import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; import { IUiSettingsClient } from 'src/core/public'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; @@ -28,6 +28,7 @@ import { getInterval } from './lib/get_interval'; import { AUTO_INTERVAL } from '../../../common/constants'; import { TIME_RANGE_DATA_MODES, PANEL_TYPES } from '../../../common/enums'; import type { IndexPattern } from '../../../../../data/common'; +import '../index.scss'; interface TimeseriesVisualizationProps { className?: string; @@ -161,18 +162,26 @@ function TimeseriesVisualization({ )} - + + + + } + > + + ); diff --git a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx index 34cc1dc347ef8..ad069a4d7e2cc 100644 --- a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx @@ -13,6 +13,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient } from 'kibana/public'; +import { EuiLoadingChart } from '@elastic/eui'; import { fetchIndexPattern } from '../common/index_patterns_utils'; import { VisualizationContainer, PersistedState } from '../../../visualizations/public'; @@ -43,10 +44,6 @@ export const getTimeseriesVisRenderer: (deps: { name: 'timeseries_vis', reuseDomNode: true, render: async (domNode, config, handlers) => { - // Build optimization. Move app styles from main bundle - // @ts-expect-error TS error, cannot find type declaration for scss - await import('./application/index.scss'); - handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); @@ -55,33 +52,49 @@ export const getTimeseriesVisRenderer: (deps: { const { indexPatterns } = getDataStart(); const showNoResult = !checkIfDataExists(visData, model); - const [palettesService, { indexPattern }] = await Promise.all([ + + let servicesLoaded; + + Promise.all([ palettes.getPalettes(), fetchIndexPattern(model.index_pattern, indexPatterns), - ]); + ]).then(([palettesService, { indexPattern }]) => { + servicesLoaded = true; - render( - - - + - - , - domNode - ); + showNoResult={showNoResult} + error={get(visData, [model.id, 'error'])} + > + + + , + domNode + ); + }); + + if (!servicesLoaded) { + render( +
+ +
, + domNode + ); + } }, }); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx similarity index 98% rename from src/plugins/visualizations/public/embeddable/visualize_embeddable.ts rename to src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 928cbec9c3747..37365fd613e5a 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -9,6 +9,9 @@ import _, { get } from 'lodash'; import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { render } from 'react-dom'; +import { EuiLoadingChart } from '@elastic/eui'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IndexPattern, @@ -299,6 +302,13 @@ export class VisualizeEmbeddable this.domNode = div; super.render(this.domNode); + render( +
+ +
, + this.domNode + ); + const expressions = getExpressions(); this.handler = await expressions.loader(this.domNode, undefined, { onRenderError: (element: HTMLElement, error: ExpressionRenderError) => { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts index c7480844adbea..2fa22cfe8d80b 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts @@ -12,10 +12,12 @@ export const createVisualizeEmbeddableAsync = async ( ...args: ConstructorParameters ) => { // Build optimization. Move app styles from main bundle - // @ts-expect-error TS error, cannot find type declaration for scss - await import('./embeddables.scss'); - const { VisualizeEmbeddable } = await import('./visualize_embeddable'); + const [{ VisualizeEmbeddable }] = await Promise.all([ + import('./visualize_embeddable'), + // @ts-expect-error TS error, cannot find type declaration for scss + import('./embeddables.scss'), + ]); return new VisualizeEmbeddable(...args); }; From cf2a3f58084370ab71b60462f386a377f7d6f1d5 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 25 Oct 2021 14:29:38 +0200 Subject: [PATCH 18/41] [Lens] Add value labels to Heatmap (#106406) * :sparkles: Add label values menu * :sparkles: Enable value labels for Heatmap * :fire: Remove removed translations * :label: Fix type issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/expressions/xy_chart/xy_args.ts | 3 +- x-pack/plugins/lens/common/types.ts | 3 + .../heatmap_visualization/chart_component.tsx | 3 + .../toolbar_component.tsx | 24 +++++- x-pack/plugins/lens/public/index.ts | 2 +- .../lens/public/shared_components/index.ts | 1 + .../value_labels_settings.test.tsx | 46 ++++++++++++ .../value_labels_settings.tsx | 75 +++++++++++++++++++ .../lens/public/xy_visualization/types.ts | 2 +- .../visual_options_popover/index.tsx | 15 ++-- .../missing_value_option.test.tsx | 53 +------------ .../missing_values_option.tsx | 66 +--------------- .../visual_options_popover.test.tsx | 10 +-- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../test/functional/apps/lens/smokescreen.ts | 2 +- 16 files changed, 171 insertions(+), 138 deletions(-) create mode 100644 x-pack/plugins/lens/public/shared_components/value_labels_settings.test.tsx create mode 100644 x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts index fb794eda22dbe..f00608135820a 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts @@ -12,8 +12,7 @@ import type { LayerArgs } from './layer_config'; import type { LegendConfigResult } from './legend_config'; import type { TickLabelsConfigResult } from './tick_labels_config'; import type { LabelsOrientationConfigResult } from './labels_orientation_config'; - -export type ValueLabelConfig = 'hide' | 'inside' | 'outside'; +import type { ValueLabelConfig } from '../../types'; export type XYCurveType = 'LINEAR' | 'CURVE_MONOTONE_X'; diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 38e198c01e730..307ed856c7c66 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -62,3 +62,6 @@ export interface CustomPaletteParams { export type RequiredPaletteParamTypes = Required; export type LayerType = 'data' | 'referenceLine'; + +// Shared by XY Chart and Heatmap as for now +export type ValueLabelConfig = 'hide' | 'inside' | 'outside'; diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index 677cb9d20c25b..c999656071ef4 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -288,6 +288,9 @@ export const HeatmapComponent: FC = ({ maxHeight: 'fill', label: { visible: args.gridConfig.isCellLabelVisible ?? false, + minFontSize: 8, + maxFontSize: 18, + useGlobalMinFontSize: true, // override the min if there's a different directive upstream }, border: { strokeWidth: 0, diff --git a/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx index 80954516f0418..88966acd22691 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Position } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import type { VisualizationToolbarProps } from '../types'; -import { LegendSettingsPopover } from '../shared_components'; +import { LegendSettingsPopover, ToolbarPopover, ValueLabelsSettings } from '../shared_components'; import type { HeatmapVisualizationState } from './types'; const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [ @@ -37,11 +37,29 @@ export const HeatmapToolbar = memo( const legendMode = state.legend.isVisible ? 'show' : 'hide'; return ( - + + + { + setState({ + ...state, + gridConfig: { ...state.gridConfig, isCellLabelVisible: newMode === 'inside' }, + }); + }} + /> + { diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 84302f25d0a02..9be07a4f44dcd 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -22,11 +22,11 @@ export type { XYLayerConfig, LegendConfig, SeriesType, - ValueLabelConfig, YAxisMode, XYCurveType, YConfig, } from '../common/expressions'; +export type { ValueLabelConfig } from '../common/types'; export type { DatatableVisualizationState } from './datatable_visualization/visualization'; export type { IndexPatternPersistedState, diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index f947ce699dce4..f268d6816910e 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -14,4 +14,5 @@ export * from './coloring'; export { useDebouncedValue } from './debounced_value'; export * from './helpers'; export { LegendActionPopover } from './legend_action_popover'; +export { ValueLabelsSettings } from './value_labels_settings'; export * from './static_header'; diff --git a/x-pack/plugins/lens/public/shared_components/value_labels_settings.test.tsx b/x-pack/plugins/lens/public/shared_components/value_labels_settings.test.tsx new file mode 100644 index 0000000000000..ae68a40d8cea6 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/value_labels_settings.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallowWithIntl as shallow } from '@kbn/test/jest'; +import { ValueLabelsSettings, VisualOptionsProps } from './value_labels_settings'; + +describe('Value labels Settings', () => { + let props: VisualOptionsProps; + beforeEach(() => { + props = { + onValueLabelChange: jest.fn(), + }; + }); + + it('should not render the component if not enabled', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lens-value-labels-visibility-btn"]').length).toEqual(0); + }); + + it('should set hide as default value', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-value-labels-visibility-btn"]').prop('idSelected') + ).toEqual(`value_labels_hide`); + }); + + it('should have called onValueLabelChange function on ButtonGroup change', () => { + const component = shallow(); + component + .find('[data-test-subj="lens-value-labels-visibility-btn"]') + .simulate('change', 'value_labels_inside'); + expect(props.onValueLabelChange).toHaveBeenCalled(); + }); + + it('should render the passed value if given', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-value-labels-visibility-btn"]').prop('idSelected') + ).toEqual(`value_labels_inside`); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx b/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx new file mode 100644 index 0000000000000..64d9f5475379a --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; +import { ValueLabelConfig } from '../../common/types'; + +const valueLabelsOptions: Array<{ + id: string; + value: ValueLabelConfig; + label: string; + 'data-test-subj': string; +}> = [ + { + id: `value_labels_hide`, + value: 'hide', + label: i18n.translate('xpack.lens.shared.valueLabelsVisibility.auto', { + defaultMessage: 'Hide', + }), + 'data-test-subj': 'lns_valueLabels_hide', + }, + { + id: `value_labels_inside`, + value: 'inside', + label: i18n.translate('xpack.lens.shared.valueLabelsVisibility.inside', { + defaultMessage: 'Show', + }), + 'data-test-subj': 'lns_valueLabels_inside', + }, +]; + +export interface VisualOptionsProps { + isVisible?: boolean; + valueLabels?: ValueLabelConfig; + onValueLabelChange: (newMode: ValueLabelConfig) => void; +} + +export const ValueLabelsSettings: FC = ({ + isVisible = true, + valueLabels = 'hide', + onValueLabelChange, +}) => { + if (!isVisible) { + return null; + } + const label = i18n.translate('xpack.lens.shared.chartValueLabelVisibilityLabel', { + defaultMessage: 'Labels', + }); + const isSelected = + valueLabelsOptions.find(({ value }) => value === valueLabels)?.id || 'value_labels_hide'; + return ( + {label}
}> + { + const newMode = valueLabelsOptions.find(({ id }) => id === modeId); + if (newMode) { + onValueLabelChange(newMode.value); + } + }} + /> + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 4729cfb96f324..475571b2965f6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -20,7 +20,6 @@ import { LensIconChartLine } from '../assets/chart_line'; import type { VisualizationType } from '../types'; import type { SeriesType, - ValueLabelConfig, LegendConfig, AxisExtentConfig, XYLayerConfig, @@ -29,6 +28,7 @@ import type { FittingFunction, LabelsOrientationConfig, } from '../../common/expressions'; +import type { ValueLabelConfig } from '../../common/types'; // Persisted parts of the state export interface XYState { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx index 2a19897445e63..8ea6f9ace6320 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ToolbarPopover, TooltipWrapper } from '../../../shared_components'; +import { ToolbarPopover, TooltipWrapper, ValueLabelsSettings } from '../../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; import { LineCurveOption } from './line_curve_option'; import { FillOpacityOption } from './fill_opacity_option'; @@ -102,14 +102,17 @@ export const VisualOptionsPopover: React.FC = ({ }} /> - { setState({ ...state, valueLabels: newMode }); }} + /> + + { setState({ ...state, fittingFunction: newVal }); }} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_value_option.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_value_option.test.tsx index 851b14839d7f7..ce4e05223b5a3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_value_option.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_value_option.test.tsx @@ -7,70 +7,23 @@ import React from 'react'; import { shallowWithIntl as shallow, mountWithIntl as mount } from '@kbn/test/jest'; -import { EuiSuperSelect, EuiButtonGroup } from '@elastic/eui'; +import { EuiSuperSelect } from '@elastic/eui'; import { MissingValuesOptions } from './missing_values_option'; describe('Missing values option', () => { it('should show currently selected fitting function', () => { const component = shallow( - + ); expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); }); - it('should show currently selected value labels display setting', () => { - const component = mount( - - ); - - expect(component.find(EuiButtonGroup).prop('idSelected')).toEqual('value_labels_inside'); - }); - - it('should show display field when enabled', () => { - const component = mount( - - ); - - expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true); - }); - - it('should hide in display value label option when disabled', () => { - const component = mount( - - ); - - expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(false); - }); - it('should show the fitting option when enabled', () => { const component = mount( ); @@ -82,9 +35,7 @@ describe('Missing values option', () => { const component = mount( ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx index b12e2d2f57112..a858d1c879efe 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx @@ -7,85 +7,23 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; import { fittingFunctionDefinitions } from '../../../../common/expressions'; -import type { FittingFunction, ValueLabelConfig } from '../../../../common/expressions'; +import type { FittingFunction } from '../../../../common/expressions'; export interface MissingValuesOptionProps { - valueLabels?: ValueLabelConfig; fittingFunction?: FittingFunction; - onValueLabelChange: (newMode: ValueLabelConfig) => void; onFittingFnChange: (newMode: FittingFunction) => void; - isValueLabelsEnabled?: boolean; isFittingEnabled?: boolean; } -const valueLabelsOptions: Array<{ - id: string; - value: 'hide' | 'inside' | 'outside'; - label: string; - 'data-test-subj': string; -}> = [ - { - id: `value_labels_hide`, - value: 'hide', - label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.auto', { - defaultMessage: 'Hide', - }), - 'data-test-subj': 'lnsXY_valueLabels_hide', - }, - { - id: `value_labels_inside`, - value: 'inside', - label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.inside', { - defaultMessage: 'Show', - }), - 'data-test-subj': 'lnsXY_valueLabels_inside', - }, -]; - export const MissingValuesOptions: React.FC = ({ - onValueLabelChange, onFittingFnChange, - valueLabels, fittingFunction, - isValueLabelsEnabled = true, isFittingEnabled = true, }) => { - const valueLabelsVisibilityMode = valueLabels || 'hide'; - return ( <> - {isValueLabelsEnabled && ( - - {i18n.translate('xpack.lens.shared.chartValueLabelVisibilityLabel', { - defaultMessage: 'Labels', - })} -
- } - > - value === valueLabelsVisibilityMode)!.id - } - onChange={(modeId) => { - const newMode = valueLabelsOptions.find(({ id }) => id === modeId)!.value; - onValueLabelChange(newMode); - }} - /> - - )} {isFittingEnabled && ( { /> ); - expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(false); + expect(component.find(ValueLabelsSettings).prop('isVisible')).toEqual(false); expect(component.find(MissingValuesOptions).prop('isFittingEnabled')).toEqual(false); }); @@ -196,7 +196,7 @@ describe('Visual options popover', () => { /> ); - expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(true); + expect(component.find(ValueLabelsSettings).prop('isVisible')).toEqual(true); }); it('should hide in the popover the display option for area and line series', () => { @@ -213,7 +213,7 @@ describe('Visual options popover', () => { /> ); - expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(false); + expect(component.find(ValueLabelsSettings).prop('isVisible')).toEqual(false); }); it('should keep the display option for bar series with multiple layers', () => { @@ -245,6 +245,6 @@ describe('Visual options popover', () => { /> ); - expect(component.find(MissingValuesOptions).prop('isValueLabelsEnabled')).toEqual(true); + expect(component.find(ValueLabelsSettings).prop('isVisible')).toEqual(true); }); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7291e94615477..a8d3b4a5f3e27 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -807,8 +807,6 @@ "xpack.lens.xyChart.topAxisDisabledHelpText": "この設定は、上の軸が有効であるときにのみ適用されます。", "xpack.lens.xyChart.topAxisLabel": "上の軸", "xpack.lens.xyChart.upperBoundLabel": "上界", - "xpack.lens.xyChart.valueLabelsVisibility.auto": "非表示", - "xpack.lens.xyChart.valueLabelsVisibility.inside": "表示", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "この設定はヒストグラムで変更できません。", "xpack.lens.xyChart.valuesInLegend.help": "凡例に値を表示", "xpack.lens.xyChart.valuesPercentageDisabledHelpText": "この設定は割合エリアグラフで変更できません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2aa9cabb7866b..4bd2a5907abf3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -817,8 +817,6 @@ "xpack.lens.xyChart.topAxisDisabledHelpText": "此设置仅在启用顶轴时应用。", "xpack.lens.xyChart.topAxisLabel": "顶轴", "xpack.lens.xyChart.upperBoundLabel": "上边界", - "xpack.lens.xyChart.valueLabelsVisibility.auto": "隐藏", - "xpack.lens.xyChart.valueLabelsVisibility.inside": "显示", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "不能在直方图上更改此设置。", "xpack.lens.xyChart.valuesInLegend.help": "在图例中显示值", "xpack.lens.xyChart.valuesPercentageDisabledHelpText": "不能在百分比面积图上更改此设置。", diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index ff5bae8aa7e61..7cacee6446723 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -256,7 +256,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show value labels on bar charts when enabled', async () => { // enable value labels await PageObjects.lens.openVisualOptions(); - await testSubjects.click('lnsXY_valueLabels_inside'); + await testSubjects.click('lns_valueLabels_inside'); await PageObjects.lens.waitForVisualization(); From 71cc3b72fe01cfd547523ce7b209d66ff04ec356 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 13:45:07 +0100 Subject: [PATCH 19/41] skip flaky suite (#115324) --- .../shared/exploratory_view/components/filter_label.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index 03fd23631f755..097ea89826c38 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -12,7 +12,8 @@ import { FilterLabel } from './filter_label'; import * as useSeriesHook from '../hooks/use_series_filters'; import { buildFilterLabel } from '../../filter_value_label/filter_value_label'; -describe('FilterLabel', function () { +// FLAKY: https://github.com/elastic/kibana/issues/115324 +describe.skip('FilterLabel', function () { mockAppIndexPattern(); const invertFilter = jest.fn(); From b59a805fcd8176b53dc859a01306669fcb0f9ba3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 13:47:38 +0100 Subject: [PATCH 20/41] skip flaky suite (#116033) --- test/functional/apps/visualize/_timelion.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index bb85b6821df31..c531ada8a2573 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -257,7 +257,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(value).to.eql('.es()'); }); - describe('dynamic suggestions for argument values', () => { + // FLAKY: https://github.com/elastic/kibana/issues/116033 + describe.skip('dynamic suggestions for argument values', () => { describe('.es()', () => { it('should show index pattern suggestions for index argument', async () => { await monacoEditor.setCodeEditorValue(''); From 1a2af9d7d2c5683f99fe104c21ffc5e6e804b03f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 13:50:03 +0100 Subject: [PATCH 21/41] skip flaky suite (#116043) --- .../public/application/hooks/use_dashboard_app_state.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index c3b4075690261..a73967ed7117b 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -276,7 +276,8 @@ describe('Dashboard initial state', () => { }); }); -describe('Dashboard state sync', () => { +// FLAKY: https://github.com/elastic/kibana/issues/116043 +describe.skip('Dashboard state sync', () => { let defaultDashboardAppStateHookResult: RenderDashboardStateHookReturn; const getResult = () => defaultDashboardAppStateHookResult.renderHookResult.result.current; From 61ea89ff7da5ada75ae973ed20da92f85f48052f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 13:54:13 +0100 Subject: [PATCH 22/41] skip flaky suite (#113080) --- .../apps/dev_tools/feature_controls/dev_tools_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 5b80f6589281a..1575948610566 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -192,7 +192,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('no dev_tools privileges', () => { + // FLAKY: https://github.com/elastic/kibana/issues/113080 + describe.skip('no dev_tools privileges', () => { before(async () => { await security.role.create('no_dev_tools_privileges_role', { kibana: [ From 35aab9e1644c3c5ad7497640fc6e1b565cf9d8af Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 13:59:07 +0100 Subject: [PATCH 23/41] skip flaky suite (#114002) --- x-pack/test/functional/apps/discover/saved_searches.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 16deecde2b0ba..f2abc6b350e5b 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; // Failing: See https://github.com/elastic/kibana/issues/104578 + // FLAKY: https://github.com/elastic/kibana/issues/114002 describe.skip('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); From a9cd15730b71151b014175a3b5a250f1b7088510 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 14:03:17 +0100 Subject: [PATCH 24/41] skip flaky suite (#115396) --- .../public/application/sections/alert_form/alert_add.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 4ae570a62f7d9..ffcda22195ff5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -64,7 +64,8 @@ export const TestExpression: FunctionComponent = () => { ); }; -describe('alert_add', () => { +// FLAKY: https://github.com/elastic/kibana/issues/g +describe.skip('alert_add', () => { let wrapper: ReactWrapper; async function setup( From 1fa073f6de4f1dd31f4c064e71250b8653d20a8b Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 14:07:43 +0100 Subject: [PATCH 25/41] skip flaky suite (#116050) --- .../public/application/hooks/use_dashboard_app_state.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index a73967ed7117b..c1e46ea2edc2a 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -206,7 +206,8 @@ describe('Dashboard container lifecycle', () => { }); }); -describe('Dashboard initial state', () => { +// FLAKY: https://github.com/elastic/kibana/issues/116050 +describe.skip('Dashboard initial state', () => { it('Extracts state from Dashboard Saved Object', async () => { const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({}); const getResult = () => renderHookResult.result.current; From de59cfe37954ea12f0c15c506de753ec882ff22f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 25 Oct 2021 14:09:51 +0100 Subject: [PATCH 26/41] skip flaky suite (#105018) --- .../public/application/hooks/use_dashboard_app_state.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index c1e46ea2edc2a..3237eb106e4ec 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -207,6 +207,7 @@ describe('Dashboard container lifecycle', () => { }); // FLAKY: https://github.com/elastic/kibana/issues/116050 +// FLAKY: https://github.com/elastic/kibana/issues/105018 describe.skip('Dashboard initial state', () => { it('Extracts state from Dashboard Saved Object', async () => { const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({}); From 125c5699446346b727a4649f39809457e0076445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 25 Oct 2021 15:19:59 +0200 Subject: [PATCH 27/41] [RAC] [Observability] Enable the observability alerting and cases features (#115785) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/observability/server/index.ts | 4 ++-- x-pack/plugins/rule_registry/server/config.ts | 2 +- .../api_integration/apis/features/features/features.ts | 1 + x-pack/test/api_integration/apis/security/privileges.ts | 1 + .../api_integration/apis/security/privileges_basic.ts | 1 + .../functional/apps/apm/feature_controls/apm_security.ts | 9 ++++++++- .../infra/feature_controls/infrastructure_security.ts | 4 ++-- .../apps/infra/feature_controls/logs_security.ts | 4 ++-- .../apps/uptime/feature_controls/uptime_security.ts | 3 ++- .../observability_functional/with_rac_write.config.ts | 4 ---- 10 files changed, 20 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index 77595d1187093..22a469dbedbdd 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -32,8 +32,8 @@ export const config: PluginConfigDescriptor = { index: schema.string({ defaultValue: 'observability-annotations' }), }), unsafe: schema.object({ - alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), - cases: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), + alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), + cases: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), }), }), }; diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts index 078498864f8e8..983a750452410 100644 --- a/x-pack/plugins/rule_registry/server/config.ts +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -12,7 +12,7 @@ export const config: PluginConfigDescriptor = { deprecations: ({ deprecate, unused }) => [unused('unsafe.indexUpgrade.enabled')], schema: schema.object({ write: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), }), unsafe: schema.object({ legacyMultiTenancy: schema.object({ diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 0e9ee6cc5ea58..d5b66c4d6da97 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -104,6 +104,7 @@ export default function ({ getService }: FtrProviderContext) { 'indexPatterns', 'graph', 'monitoring', + 'observabilityCases', 'savedObjectsManagement', 'savedObjectsTagging', 'ml', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index f234855b84e17..4938334bb936b 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -28,6 +28,7 @@ export default function ({ getService }: FtrProviderContext) { savedObjectsTagging: ['all', 'read'], canvas: ['all', 'read'], maps: ['all', 'read'], + observabilityCases: ['all', 'read'], fleet: ['all', 'read'], actions: ['all', 'read'], stackAlerts: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index ac69bfcd9d5d4..e6fe9d87af6f3 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -30,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) { savedObjectsTagging: ['all', 'read'], graph: ['all', 'read'], maps: ['all', 'read'], + observabilityCases: ['all', 'read'], canvas: ['all', 'read'], infrastructure: ['all', 'read'], logs: ['all', 'read'], diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index 7cfdf87aaf9ea..6f6d29db1ef0c 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -64,6 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const navLinks = await appsMenu.readLinks(); expect(navLinks.map((link) => link.text)).to.eql([ 'Overview', + 'Alerts', 'APM', 'User Experience', 'Stack Management', @@ -116,7 +117,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'APM', 'User Experience', 'Stack Management']); + expect(navLinks).to.eql([ + 'Overview', + 'Alerts', + 'APM', + 'User Experience', + 'Stack Management', + ]); }); it('can navigate to APM app', async () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index 65b9019f556fd..d6e8737b72b91 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -62,7 +62,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Alerts', 'Metrics', 'Stack Management']); }); describe('infrastructure landing page without data', () => { @@ -159,7 +159,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Alerts', 'Metrics', 'Stack Management']); }); describe('infrastructure landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index d120a6c582c7b..d4f56ee3c9b9b 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Alerts', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Alerts', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index 7867170c1801c..977a384062f79 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -68,6 +68,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const navLinks = await appsMenu.readLinks(); expect(navLinks.map((link) => link.text)).to.eql([ 'Overview', + 'Alerts', 'Uptime', 'Stack Management', ]); @@ -121,7 +122,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Uptime', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Alerts', 'Uptime', 'Stack Management']); }); it('can navigate to Uptime app', async () => { diff --git a/x-pack/test/observability_functional/with_rac_write.config.ts b/x-pack/test/observability_functional/with_rac_write.config.ts index 5bb7d9926af54..dcf6b121d6258 100644 --- a/x-pack/test/observability_functional/with_rac_write.config.ts +++ b/x-pack/test/observability_functional/with_rac_write.config.ts @@ -36,10 +36,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), `--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, - // TO DO: Remove feature flags once we're good to go - '--xpack.observability.unsafe.alertingExperience.enabled=true', - '--xpack.observability.unsafe.cases.enabled=true', - '--xpack.ruleRegistry.write.enabled=true', ], }, uiSettings: { From 1bc60b01157308504cee599a78591cb0043c859b Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Mon, 25 Oct 2021 17:30:35 +0300 Subject: [PATCH 28/41] [Telemetry] add missing content-type to headers (#116120) --- src/plugins/telemetry/server/fetcher.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index 02ac428b07667..97180f351986e 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -230,6 +230,7 @@ export class FetcherTask { method: 'post', body: stats, headers: { + 'Content-Type': 'application/json', 'X-Elastic-Stack-Version': this.currentKibanaVersion, 'X-Elastic-Cluster-ID': clusterUuid, 'X-Elastic-Content-Encoding': PAYLOAD_CONTENT_ENCODING, From 1527535fcd85489f16b4d91e4888b95f8f9cf32f Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 25 Oct 2021 16:58:47 +0200 Subject: [PATCH 29/41] Update alert count table to display rules with long names (#115920) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/alerts_kpis/alerts_count_panel/alerts_count.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx index f64e279fb2755..d6b1afea98592 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx @@ -35,7 +35,7 @@ const getAlertsCountTableColumns = ( { field: 'key', name: selectedStackByOption, - truncateText: true, + truncateText: false, render: function DraggableStackOptionField(value: string) { return ( Date: Mon, 25 Oct 2021 16:25:44 +0100 Subject: [PATCH 30/41] skip flaky suite (#116078) --- .../apps/ml/data_frame_analytics/feature_importance.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts index 8561487dff7cb..3faee67c01a53 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('total feature importance panel and decision path popover', function () { + // FLAKY: https://github.com/elastic/kibana/issues/116078 + describe.skip('total feature importance panel and decision path popover', function () { const testDataList: Array<{ suiteTitle: string; archive: string; From 414a1fbf0d35513a089742f1dc494e8223b7b815 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Mon, 25 Oct 2021 17:26:45 +0200 Subject: [PATCH 31/41] [Fleet] added package upgrade info logs (#116093) * added package upgrade info logs * using log meta * logging out meta object as well --- .../server/services/package_policy.test.ts | 6 +++ .../fleet/server/services/package_policy.ts | 51 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index c25a1db753c73..46747762213f1 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -578,6 +578,12 @@ describe('Package policy service', () => { }); describe('update', () => { + beforeEach(() => { + appContextService.start(createAppContextStartContractMock()); + }); + afterEach(() => { + appContextService.stop(); + }); it('should fail to update on version conflict', async () => { const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.get.mockResolvedValue({ diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index fa9df22eb5e8c..c03ccfc43ebd8 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -9,7 +9,7 @@ import { omit, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import semverLte from 'semver/functions/lte'; import { getFlattenedObject } from '@kbn/std'; -import type { KibanaRequest } from 'src/core/server'; +import type { KibanaRequest, LogMeta } from 'src/core/server'; import type { ElasticsearchClient, RequestHandlerContext, @@ -84,6 +84,17 @@ export const DATA_STREAM_ALLOWED_INDEX_PRIVILEGES = new Set([ 'read_cross_cluster', ]); +interface PackagePolicyUpgradeLogMeta extends LogMeta { + package_policy_upgrade: { + package_name: string; + current_version: string; + new_version: string; + status: 'success' | 'failure'; + error?: any[]; + dryRun?: boolean; + }; +} + class PackagePolicyService { public async create( soClient: SavedObjectsClientContract, @@ -432,6 +443,23 @@ class PackagePolicyService { pkgName: packagePolicy.package.name, currentVersion: packagePolicy.package.version, }); + + const upgradeMeta: PackagePolicyUpgradeLogMeta = { + package_policy_upgrade: { + package_name: packagePolicy.package.name, + new_version: packagePolicy.package.version, + current_version: 'unknown', + status: 'success', + dryRun: false, + }, + }; + + appContextService + .getLogger() + .info( + `Package policy successfully upgraded ${JSON.stringify(upgradeMeta)}`, + upgradeMeta + ); } return newPolicy; @@ -661,6 +689,27 @@ class PackagePolicyService { const hasErrors = 'errors' in updatedPackagePolicy; + if (packagePolicy.package.version !== packageInfo.version) { + const upgradeMeta: PackagePolicyUpgradeLogMeta = { + package_policy_upgrade: { + package_name: packageInfo.name, + current_version: packagePolicy.package.version, + new_version: packageInfo.version, + status: hasErrors ? 'failure' : 'success', + error: hasErrors ? updatedPackagePolicy.errors : undefined, + dryRun: true, + }, + }; + appContextService + .getLogger() + .info( + `Package policy upgrade dry run ${ + hasErrors ? 'resulted in errors' : 'ran successfully' + } ${JSON.stringify(upgradeMeta)}`, + upgradeMeta + ); + } + return { name: updatedPackagePolicy.name, diff: [packagePolicy, updatedPackagePolicy], From 80bb1ad8dac3a7d5c28897629e8c34c14d384816 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 25 Oct 2021 17:48:36 +0200 Subject: [PATCH 32/41] [Security Solution] Replace badge with health indicator in endpoint details flyout (#115901) --- .../components/endpoint_details_tabs.tsx | 2 +- .../view/details/endpoint_details_content.tsx | 50 ++++++--------- .../pages/endpoint_hosts/view/index.test.tsx | 61 +++++++------------ 3 files changed, 41 insertions(+), 72 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx index 8f044959f4c90..adae21b55a637 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx @@ -62,7 +62,7 @@ export const EndpointDetailsFlyoutTabs = memo( const selectedTab = useMemo(() => tabs.find((tab) => tab.id === show), [tabs, show]); const renderTabs = tabs.map((tab) => ( - + )); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx index cc1cad52eb21c..cb920bdbd1b03 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx @@ -13,8 +13,9 @@ import { EuiText, EuiFlexGroup, EuiFlexItem, - EuiBadge, EuiSpacer, + EuiLink, + EuiHealth, } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -30,7 +31,6 @@ import { getEndpointDetailsPath } from '../../../../common/routing'; import { EndpointPolicyLink } from '../components/endpoint_policy_link'; import { OutOfDate } from '../components/out_of_date'; import { EndpointAgentStatus } from '../components/endpoint_agent_status'; -import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -53,9 +53,8 @@ export const EndpointDetailsContent = memo( const policyStatus = useEndpointSelector( policyResponseStatus ) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR; - const { getAppUrl } = useAppUrl(); - const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { + const policyResponseRoutePath = useMemo(() => { // eslint-disable-next-line @typescript-eslint/naming-convention const { selected_endpoint, show, ...currentUrlParams } = queryParams; const path = getEndpointDetailsPath({ @@ -63,8 +62,8 @@ export const EndpointDetailsContent = memo( ...currentUrlParams, selected_endpoint: details.agent.id, }); - return [getAppUrl({ path }), path]; - }, [details.agent.id, getAppUrl, queryParams]); + return path; + }, [details.agent.id, queryParams]); const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); @@ -142,26 +141,20 @@ export const EndpointDetailsContent = memo( defaultMessage: 'Policy Status', }), description: ( - // https://github.com/elastic/eui/issues/4530 - // @ts-ignore - - - - - + + + + + + ), }, { @@ -185,14 +178,7 @@ export const EndpointDetailsContent = memo( ), }, ]; - }, [ - details, - hostStatus, - policyResponseUri, - policyStatus, - policyStatusClickHandler, - policyInfo, - ]); + }, [details, hostStatus, policyStatus, policyStatusClickHandler, policyInfo]); return ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 727c2e8a35024..45dcf5f7a0f7d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -167,7 +167,7 @@ describe('when on the endpoint list page', () => { it('should NOT display timeline', async () => { const renderResult = render(); - const timelineFlyout = await renderResult.queryByTestId('flyoutOverlay'); + const timelineFlyout = renderResult.queryByTestId('flyoutOverlay'); expect(timelineFlyout).toBeNull(); }); @@ -460,7 +460,7 @@ describe('when on the endpoint list page', () => { const outOfDates = await renderResult.findAllByTestId('rowPolicyOutOfDate'); expect(outOfDates).toHaveLength(4); - outOfDates.forEach((item, index) => { + outOfDates.forEach((item) => { expect(item.textContent).toEqual('Out-of-date'); expect(item.querySelector(`[data-euiicon-type][color=warning]`)).not.toBeNull(); }); @@ -512,8 +512,8 @@ describe('when on the endpoint list page', () => { // FLAKY: https://github.com/elastic/kibana/issues/75721 describe.skip('when polling on Endpoint List', () => { - beforeEach(async () => { - await reactTestingLibrary.act(() => { + beforeEach(() => { + reactTestingLibrary.act(() => { const hostListData = mockEndpointResultList({ total: 4 }).hosts; setEndpointListApiMockImplementation(coreStart.http, { @@ -703,8 +703,8 @@ describe('when on the endpoint list page', () => { it('should show the flyout and footer', async () => { const renderResult = await renderAndWaitForData(); - await expect(renderResult.findByTestId('endpointDetailsFlyout')).not.toBeNull(); - await expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).not.toBeNull(); + expect(renderResult.getByTestId('endpointDetailsFlyout')).not.toBeNull(); + expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).not.toBeNull(); }); it('should display policy name value as a link', async () => { @@ -738,15 +738,6 @@ describe('when on the endpoint list page', () => { ); }); - it('should display policy status value as a link', async () => { - const renderResult = await renderAndWaitForData(); - const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); - expect(policyStatusLink).not.toBeNull(); - expect(policyStatusLink.getAttribute('href')).toEqual( - `${APP_PATH}${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=policy_response` - ); - }); - it('should update the URL when policy status link is clicked', async () => { const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); @@ -763,10 +754,8 @@ describe('when on the endpoint list page', () => { it('should display Success overall policy status', async () => { const renderResult = await renderAndWaitForData(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); + expect(renderResult.getByTestId('policyStatusValue-success')).toBeTruthy(); expect(policyStatusBadge.textContent).toEqual('Success'); - expect(policyStatusBadge.getAttribute('style')).toMatch( - /background-color\: rgb\(109\, 204\, 177\)\;/ - ); }); it('should display Warning overall policy status', async () => { @@ -774,9 +763,7 @@ describe('when on the endpoint list page', () => { const renderResult = await renderAndWaitForData(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusBadge.textContent).toEqual('Warning'); - expect(policyStatusBadge.getAttribute('style')).toMatch( - /background-color\: rgb\(241\, 216\, 111\)\;/ - ); + expect(renderResult.getByTestId('policyStatusValue-warning')).toBeTruthy(); }); it('should display Failed overall policy status', async () => { @@ -784,9 +771,7 @@ describe('when on the endpoint list page', () => { const renderResult = await renderAndWaitForData(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusBadge.textContent).toEqual('Failed'); - expect(policyStatusBadge.getAttribute('style')).toMatch( - /background-color\: rgb\(255\, 126\, 98\)\;/ - ); + expect(renderResult.getByTestId('policyStatusValue-failure')).toBeTruthy(); }); it('should display Unknown overall policy status', async () => { @@ -794,9 +779,7 @@ describe('when on the endpoint list page', () => { const renderResult = await renderAndWaitForData(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusBadge.textContent).toEqual('Unknown'); - expect(policyStatusBadge.getAttribute('style')).toMatch( - /background-color\: rgb\(211\, 218\, 230\)\;/ - ); + expect(renderResult.getByTestId('policyStatusValue-')).toBeTruthy(); }); it('should show the Take Action button', async () => { @@ -898,7 +881,7 @@ describe('when on the endpoint list page', () => { await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); - const hostNameLinks = await renderResult.getAllByTestId('hostnameCellLink'); + const hostNameLinks = renderResult.getAllByTestId('hostnameCellLink'); reactTestingLibrary.fireEvent.click(hostNameLinks[0]); }); @@ -913,7 +896,7 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.act(() => { dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); - const endpointDetailsFlyout = await renderResult.queryByTestId('endpointDetailsFlyoutBody'); + const endpointDetailsFlyout = renderResult.queryByTestId('endpointDetailsFlyoutBody'); expect(endpointDetailsFlyout).not.toBeNull(); }); @@ -926,7 +909,7 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.act(() => { dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); - const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + const logEntries = renderResult.queryAllByTestId('timelineEntry'); expect(logEntries.length).toEqual(3); expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null); @@ -944,7 +927,7 @@ describe('when on the endpoint list page', () => { getMockData({ hasLogsEndpointActionResponses: true }) ); }); - const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + const logEntries = renderResult.queryAllByTestId('timelineEntry'); expect(logEntries.length).toEqual(4); expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); expect(`${logEntries[1]} .euiCommentTimeline__icon--update`).not.toBe(null); @@ -960,7 +943,7 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.act(() => { dispatchEndpointDetailsActivityLogChanged('failed', getMockData()); }); - const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + const emptyState = renderResult.queryByTestId('activityLogEmpty'); expect(emptyState).not.toBe(null); }); @@ -980,10 +963,10 @@ describe('when on the endpoint list page', () => { }); }); - const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + const emptyState = renderResult.queryByTestId('activityLogEmpty'); expect(emptyState).toBe(null); - const superDatePicker = await renderResult.queryByTestId('activityLogSuperDatePicker'); + const superDatePicker = renderResult.queryByTestId('activityLogSuperDatePicker'); expect(superDatePicker).not.toBe(null); }); @@ -1002,7 +985,7 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.act(() => { dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); - const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + const logEntries = renderResult.queryAllByTestId('timelineEntry'); expect(logEntries.length).toEqual(3); }); @@ -1047,7 +1030,7 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.act(() => { dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); - const commentTexts = await renderResult.queryAllByTestId('activityLogCommentText'); + const commentTexts = renderResult.queryAllByTestId('activityLogCommentText'); expect(commentTexts.length).toEqual(1); expect(commentTexts[0].textContent).toEqual('some comment'); expect(commentTexts[0].parentElement?.parentElement?.className).toContain( @@ -1081,7 +1064,7 @@ describe('when on the endpoint list page', () => { afterEach(reactTestingLibrary.cleanup); it('should hide the host details panel', async () => { - const endpointDetailsFlyout = await renderResult.queryByTestId('endpointDetailsFlyoutBody'); + const endpointDetailsFlyout = renderResult.queryByTestId('endpointDetailsFlyoutBody'); expect(endpointDetailsFlyout).toBeNull(); }); @@ -1328,8 +1311,8 @@ describe('when on the endpoint list page', () => { ).toBe(true); }); - it('should NOT show the flyout footer', async () => { - await expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).toBeNull(); + it('should NOT show the flyout footer', () => { + expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).toBeNull(); }); }); }); From 4cc94c5a45e616b23e34aaac8dab28033edff3ec Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 25 Oct 2021 16:54:32 +0100 Subject: [PATCH 33/41] [ML] Fixing index data visualizer not available when no ML nodes available (#115972) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../routing/routes/datavisualizer/index_based.tsx | 7 ------- .../application/routing/routes/new_job/index_or_search.tsx | 7 ------- 2 files changed, 14 deletions(-) diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index b9e866d2e00d0..04543a28ab3e6 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -19,10 +19,7 @@ import { IndexDataVisualizerPage as Page } from '../../../datavisualizer/index_b import { checkBasicLicense } from '../../../license'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { checkMlNodesAvailable } from '../../../ml_nodes_check'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -import { ML_PAGES } from '../../../../../common/constants/locator'; -import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; export const indexBasedRouteFactory = ( navigateToPath: NavigateToPath, @@ -44,16 +41,12 @@ export const indexBasedRouteFactory = ( const PageWrapper: FC = ({ location, deps }) => { const { redirectToMlAccessDeniedPage } = deps; - const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( - ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE - ); const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), - checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index 8500d85d5580a..53057cb16c132 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -19,9 +19,6 @@ import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; -import { checkMlNodesAvailable } from '../../../ml_nodes_check'; -import { ML_PAGES } from '../../../../../common/constants/locator'; -import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; enum MODE { NEW_JOB, @@ -85,9 +82,6 @@ const PageWrapper: FC = ({ nextStepPath, deps, mode }) = } = useMlKibana(); const { redirectToMlAccessDeniedPage } = deps; - const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( - ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE - ); const newJobResolvers = { ...basicResolvers(deps), @@ -98,7 +92,6 @@ const PageWrapper: FC = ({ nextStepPath, deps, mode }) = checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), - checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }; const { context } = useResolver( From d91bc28846762f916c5c8cc5faa4e6f6a61107f3 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Mon, 25 Oct 2021 12:04:57 -0400 Subject: [PATCH 34/41] Conditionally sets ignore_throttled only when search:includeFrozen is true (#115451) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../framework/kibana_framework_adapter.ts | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 4576a2e8452ac..b1ea0ce21b3c1 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -5,37 +5,41 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { IndicesExistsAlias, IndicesGet, MlGetBuckets, } from '@elastic/elasticsearch/api/requestParams'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; -import { estypes } from '@elastic/elasticsearch'; -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; -import { - InfraRouteConfig, - InfraServerPluginSetupDeps, - CallWithRequestParams, - InfraDatabaseSearchResponse, - InfraDatabaseMultiResponse, - InfraDatabaseFieldCapsResponse, - InfraDatabaseGetIndicesResponse, - InfraDatabaseGetIndicesAliasResponse, -} from './adapter_types'; -import { TSVBMetricModel } from '../../../../common/inventory_models/types'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { CoreSetup, IRouter, KibanaRequest, + RequestHandler, RouteMethod, } from '../../../../../../../src/core/server'; -import { RequestHandler } from '../../../../../../../src/core/server'; -import { InfraConfig } from '../../../plugin'; -import type { InfraPluginRequestHandlerContext } from '../../../types'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; import { TimeseriesVisData } from '../../../../../../../src/plugins/vis_types/timeseries/server'; -import { InfraServerPluginStartDeps } from './adapter_types'; +import { TSVBMetricModel } from '../../../../common/inventory_models/types'; +import { InfraConfig } from '../../../plugin'; +import type { InfraPluginRequestHandlerContext } from '../../../types'; +import { + CallWithRequestParams, + InfraDatabaseFieldCapsResponse, + InfraDatabaseGetIndicesAliasResponse, + InfraDatabaseGetIndicesResponse, + InfraDatabaseMultiResponse, + InfraDatabaseSearchResponse, + InfraRouteConfig, + InfraServerPluginSetupDeps, + InfraServerPluginStartDeps, +} from './adapter_types'; + +interface FrozenIndexParams { + ignore_throttled?: boolean; +} export class KibanaFramework { public router: IRouter; @@ -133,7 +137,7 @@ export class KibanaFramework { ) { const { elasticsearch, uiSettings } = requestContext.core; - const includeFrozen = await uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); + const includeFrozen = await uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); if (endpoint === 'msearch') { const maxConcurrentShardRequests = await uiSettings.client.get( UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS @@ -143,11 +147,17 @@ export class KibanaFramework { } } - const frozenIndicesParams = ['search', 'msearch'].includes(endpoint) - ? { - ignore_throttled: !includeFrozen, - } - : {}; + // Only set the "ignore_throttled" value (to false) if the Kibana setting + // for "search:includeFrozen" is true (i.e. don't ignore throttled indices, a triple negative!) + // More information: + // - https://github.com/elastic/kibana/issues/113197 + // - https://github.com/elastic/elasticsearch/pull/77479 + // + // NOTE: these params only need to be spread onto the search and msearch calls below + const frozenIndicesParams: FrozenIndexParams = {}; + if (includeFrozen) { + frozenIndicesParams.ignore_throttled = false; + } let apiResult; switch (endpoint) { @@ -166,37 +176,31 @@ export class KibanaFramework { case 'fieldCaps': apiResult = elasticsearch.client.asCurrentUser.fieldCaps({ ...params, - ...frozenIndicesParams, }); break; case 'indices.existsAlias': apiResult = elasticsearch.client.asCurrentUser.indices.existsAlias({ ...params, - ...frozenIndicesParams, } as IndicesExistsAlias); break; case 'indices.getAlias': apiResult = elasticsearch.client.asCurrentUser.indices.getAlias({ ...params, - ...frozenIndicesParams, }); break; case 'indices.get': apiResult = elasticsearch.client.asCurrentUser.indices.get({ ...params, - ...frozenIndicesParams, } as IndicesGet); break; case 'transport.request': apiResult = elasticsearch.client.asCurrentUser.transport.request({ ...params, - ...frozenIndicesParams, } as TransportRequestParams); break; case 'ml.getBuckets': apiResult = elasticsearch.client.asCurrentUser.ml.getBuckets({ ...params, - ...frozenIndicesParams, } as MlGetBuckets); break; } From 1e718a557211e1eda5fd1282c171c43daebff3fe Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 25 Oct 2021 11:10:16 -0500 Subject: [PATCH 35/41] [data views] Make data view saved objects share capable (#114408) --- .../server/saved_objects/data_views.ts | 3 +- .../apis/saved_objects_management/find.ts | 2 +- .../saved_objects_management/relationships.ts | 4 +- .../saved_objects/spaces/data.json | 23 ++++--- .../common/suites/copy_to_space.ts | 15 +++-- .../suites/resolve_copy_to_space_conflicts.ts | 67 ++++++++++--------- .../apis/resolve_copy_to_space_conflicts.ts | 5 +- 7 files changed, 67 insertions(+), 52 deletions(-) diff --git a/src/plugins/data_views/server/saved_objects/data_views.ts b/src/plugins/data_views/server/saved_objects/data_views.ts index 5bb85a9bb6e98..ca7592732c3ee 100644 --- a/src/plugins/data_views/server/saved_objects/data_views.ts +++ b/src/plugins/data_views/server/saved_objects/data_views.ts @@ -13,7 +13,8 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common'; export const dataViewSavedObjectType: SavedObjectsType = { name: DATA_VIEW_SAVED_OBJECT_TYPE, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', management: { displayName: 'Data view', icon: 'indexPatternApp', diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 0b744b7991b38..ea7f297dfeb08 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -249,7 +249,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index 518ec29947016..838bc05346dda 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -91,7 +91,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, }, @@ -132,7 +132,7 @@ export default function ({ getService }: FtrProviderContext) { path: '/app/management/kibana/dataViews/dataView/8963ca30-3224-11e8-a572-ffca06da1357', uiCapabilitiesPath: 'management.kibana.indexPatterns', }, - namespaceType: 'single', + namespaceType: 'multiple-isolated', hiddenType: false, }, relationship: 'child', diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 3b2a87d924e88..c1525409cfa3f 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -153,7 +153,7 @@ "references": [ { "type": "index-pattern", - "id": "cts_ip_1", + "id": "cts_ip_1_default", "name": "CTS IP 1" } ], @@ -182,7 +182,7 @@ "references": [ { "type": "index-pattern", - "id": "cts_ip_1", + "id": "cts_ip_1_default", "name": "CTS IP 1" } ], @@ -212,7 +212,7 @@ "references": [ { "type": "index-pattern", - "id": "cts_ip_1", + "id": "cts_ip_1_default", "name": "CTS IP 1" } ], @@ -276,7 +276,7 @@ "references": [ { "type": "index-pattern", - "id": "cts_ip_1", + "id": "cts_ip_1_space_1", "name": "CTS IP 1" } ], @@ -305,7 +305,7 @@ "references": [ { "type": "index-pattern", - "id": "cts_ip_1", + "id": "cts_ip_1_space_1", "name": "CTS IP 1" } ], @@ -335,7 +335,7 @@ "references": [ { "type": "index-pattern", - "id": "cts_ip_1", + "id": "cts_ip_1_space_1", "name": "CTS IP 1" } ], @@ -350,15 +350,17 @@ { "type": "_doc", "value": { - "id": "index-pattern:cts_ip_1", + "id": "index-pattern:cts_ip_1_default", "index": ".kibana", "source": { + "originId": "cts_ip_1", "index-pattern": { "title": "Copy to Space index pattern 1 from default space" }, "references": [], "type": "index-pattern", - "updated_at": "2017-09-21T18:49:16.270Z" + "updated_at": "2017-09-21T18:49:16.270Z", + "namespaces": ["default"] }, "type": "_doc" } @@ -367,16 +369,17 @@ { "type": "_doc", "value": { - "id": "space_1:index-pattern:cts_ip_1", + "id": "index-pattern:cts_ip_1_space_1", "index": ".kibana", "source": { + "originId": "cts_ip_1", "index-pattern": { "title": "Copy to Space index pattern 1 from space_1 space" }, "references": [], "type": "index-pattern", "updated_at": "2017-09-21T18:49:16.270Z", - "namespace": "space_1" + "namespaces": ["space_1"] }, "type": "_doc" } diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 3a3f0f889c91c..23136838f3002 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -183,6 +183,8 @@ export function copyToSpaceTestSuiteFactory( const destination = getDestinationWithoutConflicts(); const result = resp.body as CopyResponse; + const indexPatternDestinationId = result[destination].successResults![0].destinationId; + expect(indexPatternDestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID const vis1DestinationId = result[destination].successResults![1].destinationId; expect(vis1DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID const vis2DestinationId = result[destination].successResults![2].destinationId; @@ -196,12 +198,13 @@ export function copyToSpaceTestSuiteFactory( successCount: 5, successResults: [ { - id: 'cts_ip_1', + id: `cts_ip_1_${spaceId}`, type: 'index-pattern', meta: { icon: 'indexPatternApp', title: `Copy to Space index pattern 1 from ${spaceId} space`, }, + destinationId: indexPatternDestinationId, }, { id: `cts_vis_1_${spaceId}`, @@ -321,13 +324,14 @@ export function copyToSpaceTestSuiteFactory( successCount: 5, successResults: [ { - id: 'cts_ip_1', + id: `cts_ip_1_${spaceId}`, type: 'index-pattern', meta: { icon: 'indexPatternApp', title: `Copy to Space index pattern 1 from ${spaceId} space`, }, overwrite: true, + destinationId: `cts_ip_1_${destination}`, // this conflicted with another index pattern in the destination space because of a shared originId }, { id: `cts_vis_1_${spaceId}`, @@ -409,8 +413,11 @@ export function copyToSpaceTestSuiteFactory( }, }, { - error: { type: 'conflict' }, - id: 'cts_ip_1', + error: { + type: 'conflict', + destinationId: `cts_ip_1_${destination}`, // this conflicted with another index pattern in the destination space because of a shared originId + }, + id: `cts_ip_1_${spaceId}`, title: `Copy to Space index pattern 1 from ${spaceId} space`, type: 'index-pattern', meta: { diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index b190a37965b0b..72130743b69d9 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -82,8 +82,18 @@ export function resolveCopyToSpaceConflictsSuite( expect(result).to.eql({ [destination]: { success: true, - successCount: 1, + successCount: 2, successResults: [ + { + id: `cts_ip_1_${sourceSpaceId}`, + type: 'index-pattern', + meta: { + title: `Copy to Space index pattern 1 from ${sourceSpaceId} space`, + icon: 'indexPatternApp', + }, + destinationId: `cts_ip_1_${destination}`, // this conflicted with another index pattern in the destination space because of a shared originId + overwrite: true, + }, { id: `cts_vis_3_${sourceSpaceId}`, type: 'visualization', @@ -146,6 +156,19 @@ export function resolveCopyToSpaceConflictsSuite( success: false, successCount: 0, errors: [ + { + error: { + type: 'conflict', + destinationId: `cts_ip_1_${destination}`, // this conflicted with another index pattern in the destination space because of a shared originId + }, + id: `cts_ip_1_${sourceSpaceId}`, + title: `Copy to Space index pattern 1 from ${sourceSpaceId} space`, + meta: { + title: `Copy to Space index pattern 1 from ${sourceSpaceId} space`, + icon: 'indexPatternApp', + }, + type: 'index-pattern', + }, { error: { type: 'conflict', @@ -231,35 +254,7 @@ export function resolveCopyToSpaceConflictsSuite( { statusCode: 403, error: 'Forbidden', - message: 'Unable to bulk_get index-pattern', - }, - ], - }, - } as CopyResponse); - - // Query ES to ensure that nothing was copied - const [dashboard, visualization] = await getObjectsAtSpace(destination); - expect(dashboard.attributes.title).to.eql( - `This is the ${destination} test space CTS dashboard` - ); - expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`); - }; - - const createExpectReadonlyAtSpaceWithReferencesResult = - (spaceId: string = DEFAULT_SPACE_ID) => - async (resp: TestResponse) => { - const destination = getDestinationSpace(spaceId); - - const result = resp.body as CopyResponse; - expect(result).to.eql({ - [destination]: { - success: false, - successCount: 0, - errors: [ - { - statusCode: 403, - error: 'Forbidden', - message: 'Unable to bulk_create visualization', + message: 'Unable to bulk_create index-pattern,visualization', }, ], }, @@ -449,6 +444,7 @@ export function resolveCopyToSpaceConflictsSuite( const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' }; const visualizationObject = { type: 'visualization', id: `cts_vis_3_${spaceId}` }; + const indexPatternObject = { type: 'index-pattern', id: `cts_ip_1_${spaceId}` }; it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => { const destination = getDestinationSpace(spaceId); @@ -462,6 +458,11 @@ export function resolveCopyToSpaceConflictsSuite( createNewCopies: false, retries: { [destination]: [ + { + ...indexPatternObject, + destinationId: `cts_ip_1_${destination}`, + overwrite: false, + }, { ...visualizationObject, destinationId: `cts_vis_3_${destination}`, @@ -486,6 +487,11 @@ export function resolveCopyToSpaceConflictsSuite( createNewCopies: false, retries: { [destination]: [ + { + ...indexPatternObject, + destinationId: `cts_ip_1_${destination}`, + overwrite: true, + }, { ...visualizationObject, destinationId: `cts_vis_3_${destination}`, @@ -589,7 +595,6 @@ export function resolveCopyToSpaceConflictsSuite( createExpectNonOverriddenResponseWithReferences, createExpectNonOverriddenResponseWithoutReferences, createExpectUnauthorizedAtSpaceWithReferencesResult, - createExpectReadonlyAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, createMultiNamespaceTestCases, originSpaces: ['default', 'space_1'], diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts index efc7827cf8b9a..1b39cd5d77302 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts @@ -24,7 +24,6 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr createExpectOverriddenResponseWithoutReferences, expectRouteForbiddenResponse, createExpectUnauthorizedAtSpaceWithReferencesResult, - createExpectReadonlyAtSpaceWithReferencesResult, createExpectUnauthorizedAtSpaceWithoutReferencesResult, createMultiNamespaceTestCases, NON_EXISTENT_SPACE_ID, @@ -122,11 +121,11 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr tests: { withReferencesNotOverwriting: { statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId), + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId), }, withReferencesOverwriting: { statusCode: 200, - response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId), + response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId), }, withoutReferencesOverwriting: { statusCode: 200, From 33fd1bdff0556e899e37e6885b3c11f7c4bc33ac Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 25 Oct 2021 12:41:04 -0400 Subject: [PATCH 36/41] [Maps] Use ES mvt (#114553) * tmp * tmp * tmp * tmp * tmp * use es naming * typo * organize files for clarity * plugin for hits * tmp * initial styling * more boilerplate * tmp * temp * add size support * remove junk * tooltip * edits * too many features * rename for clarity * typing * tooltip improvements * icon * callouts * align count handling * typechecks * i18n * tmp * type fixes * linting * convert to ts and disable option * readd test dependencies * typescheck * update yarn lock * fix typecheck * update snapshot * fix snapshot * fix snapshot * fix snapshot * fix snapshot * fix test * fix tests * fix test * add key * fix integration test * move test * use centroid placement * more text fixes * more test fixes * Remove top terms aggregations when switching to super fine resolution (#114667) * [Maps] MVT metrics * remove js file * updateSourceProps * i18n cleanup * mvt labels * remove isPointsOnly from IVectorSource interface * move get_centroid_featues to vector_layer since its no longer used in server * labels * warn users when selecting scaling type that does not support term joins * clean up scaling_form * remove IField.isCountable method * move pluck code from common to dynamic_style_property * move convert_to_geojson to es_geo_grid_source folder * remove getMbFeatureIdPropertyName from IVectorLayer * clean up cleanTooltipStateForLayer * use euiWarningColor for too many features outline * update jest snapshots and eslint fixes * update docs for incomplete data changes * move tooManyFeatures MB layer definition from VectorLayer to TiledVectorLayer, clean up VectorSource interface * remove commented out filter in tooltip_control add api docs for getMbLayerIds and getMbTooltipLayerIds * revert changing getSourceTooltipContent to getSourceTooltipConfigFromGeoJson * replace DEFAULT_MAX_RESULT_WINDOW with loading maxResultWindow as data request * clean up * eslint * remove unused constants from Kibana MVT implemenation and tooManyFeaturesImage * add better should method for tiled_vector_layer.getCustomIconAndTooltipContent jest test * fix tooltips not being displayed for super-fine clusters and grids * fix check in getFeatureId for es_Search_sources only * eslint, remove __kbn_metadata_feature__ filter from mapbox style expects * remove geoFieldType paramter for tile API * remove searchSessionId from MVT url since its no longer used * tslint * vector tile scaling option copy update * fix getTile and getGridTile API integration tests * remove size from _mvt request body, size provided in query * eslint, fix test expect * stablize jest test * track total hits for _mvt request * track total hits take 2 * align vector tile copy * eslint * revert change to EsSearchSource._loadTooltipProperties with regards to handling undefined _index. MVT now provides _index * clean up * only send metric aggregations to mvt/getGridTile endpoint * update snapshot, update getGridTile URLs in tests * update request URL for getGridTile * eslint Co-authored-by: Nathan Reese Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/maps/vector-layer.asciidoc | 4 +- package.json | 2 +- x-pack/plugins/maps/common/constants.ts | 10 +- .../layer_descriptor_types.ts | 17 +- .../maps/common/elasticsearch_util/index.ts | 1 - .../maps/common/get_geometry_counts.ts | 45 -- .../maps/common/pluck_category_field_meta.ts | 46 -- .../maps/common/pluck_range_field_meta.ts | 34 -- .../public/actions/data_request_actions.ts | 21 +- .../maps/public/actions/layer_actions.ts | 32 +- .../maps/public/actions/map_actions.ts | 4 +- .../maps/public/actions/tooltip_actions.ts | 15 +- .../public/classes/fields/agg/agg_field.ts | 4 + .../classes/fields/agg/count_agg_field.ts | 4 + .../fields/agg/top_term_percentage_field.ts | 4 + .../maps/public/classes/fields/field.ts | 5 + .../maps/public/classes/layers/layer.tsx | 16 +- .../tiled_vector_layer.test.tsx.snap | 14 +- .../tiled_vector_layer.test.tsx | 4 +- .../tiled_vector_layer/tiled_vector_layer.tsx | 202 +++++-- .../vector_layer/assign_feature_ids.test.ts | 17 +- .../layers/vector_layer/assign_feature_ids.ts | 18 +- .../get_centroid_features.test.ts | 29 - .../vector_layer}/get_centroid_features.ts | 7 +- .../classes/layers/vector_layer/index.ts | 1 + .../classes/layers/vector_layer/utils.tsx | 2 +- .../layers/vector_layer/vector_layer.tsx | 101 ++-- .../sources/es_agg_source/es_agg_source.ts | 8 +- .../resolution_editor.test.tsx.snap | 136 ++--- .../update_source_editor.test.tsx.snap | 9 +- .../convert_to_geojson.d.ts | 2 +- .../es_geo_grid_source}/convert_to_geojson.js | 11 +- .../convert_to_geojson.test.ts | 2 +- .../es_geo_grid_source.test.ts | 21 +- .../es_geo_grid_source/es_geo_grid_source.tsx | 38 +- .../es_geo_grid_source/resolution_editor.js | 61 -- .../resolution_editor.test.tsx | 2 +- .../es_geo_grid_source/resolution_editor.tsx | 161 ++++++ .../update_source_editor.test.tsx | 9 +- ...rce_editor.js => update_source_editor.tsx} | 78 ++- .../es_search_source/create_source_editor.js | 2 + .../es_search_source/es_search_source.test.ts | 20 +- .../es_search_source/es_search_source.tsx | 31 +- .../es_search_source/update_source_editor.js | 4 + .../__snapshots__/scaling_form.test.tsx.snap | 24 +- .../util/scaling_form.test.tsx | 2 + .../es_search_source/util/scaling_form.tsx | 134 +++-- .../classes/sources/es_source/es_source.ts | 2 +- .../mvt_field_config_editor.test.tsx.snap | 5 - .../mvt_field_config_editor.tsx | 6 +- .../mvt_single_layer_vector_source.tsx | 4 + .../maps/public/classes/sources/source.ts | 11 +- .../sources/vector_source/vector_source.tsx | 7 +- .../vector_style_editor.test.tsx.snap | 540 ------------------ .../components/vector_style_editor.test.tsx | 46 +- .../vector/components/vector_style_editor.tsx | 20 +- .../properties/dynamic_color_property.tsx | 6 +- .../properties/dynamic_icon_property.tsx | 4 +- .../properties/dynamic_size_property.tsx | 4 +- .../properties/dynamic_style_property.tsx | 121 ++-- .../classes/styles/vector/vector_style.tsx | 61 +- .../classes/util/geo_tile_utils.test.ts} | 0 .../classes/util}/geo_tile_utils.ts | 6 +- .../classes/util/mb_filter_expressions.ts | 21 +- .../edit_layer_panel.test.tsx.snap | 4 + .../edit_layer_panel.test.tsx | 10 +- .../edit_layer_panel/edit_layer_panel.tsx | 23 +- .../edit_layer_panel/index.ts | 12 +- .../join_editor/join_editor.tsx | 2 - .../draw_feature_control.tsx | 7 +- .../connected_components/mb_map/mb_map.tsx | 9 - .../tooltip_control/tooltip_control.test.tsx | 13 + .../tooltip_control/tooltip_control.tsx | 31 +- .../maps/server/kibana_server_services.ts | 3 + .../plugins/maps/server/mvt/get_grid_tile.ts | 64 +++ x-pack/plugins/maps/server/mvt/get_tile.ts | 437 +------------- x-pack/plugins/maps/server/mvt/mvt_routes.ts | 37 +- x-pack/plugins/maps/server/mvt/util.ts | 75 --- .../apis/maps/get_grid_tile.js | 109 ++-- .../api_integration/apis/maps/get_tile.js | 78 +-- .../functional/apps/maps/mapbox_styles.js | 28 - .../test/functional/apps/maps/mvt_scaling.js | 31 +- .../functional/apps/maps/mvt_super_fine.js | 6 +- 83 files changed, 1201 insertions(+), 2056 deletions(-) delete mode 100644 x-pack/plugins/maps/common/get_geometry_counts.ts delete mode 100644 x-pack/plugins/maps/common/pluck_category_field_meta.ts delete mode 100644 x-pack/plugins/maps/common/pluck_range_field_meta.ts rename x-pack/plugins/maps/{common => public/classes/layers/vector_layer}/get_centroid_features.test.ts (93%) rename x-pack/plugins/maps/{common => public/classes/layers/vector_layer}/get_centroid_features.ts (94%) rename x-pack/plugins/maps/{common/elasticsearch_util => public/classes/sources/es_geo_grid_source}/convert_to_geojson.d.ts (89%) rename x-pack/plugins/maps/{common/elasticsearch_util => public/classes/sources/es_geo_grid_source}/convert_to_geojson.js (89%) rename x-pack/plugins/maps/{common/elasticsearch_util => public/classes/sources/es_geo_grid_source}/convert_to_geojson.test.ts (98%) delete mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js create mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx rename x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/{update_source_editor.js => update_source_editor.tsx} (61%) rename x-pack/plugins/maps/{common/geo_tile_utils.test.js => public/classes/util/geo_tile_utils.test.ts} (100%) rename x-pack/plugins/maps/{common => public/classes/util}/geo_tile_utils.ts (95%) create mode 100644 x-pack/plugins/maps/server/mvt/get_grid_tile.ts delete mode 100644 x-pack/plugins/maps/server/mvt/util.ts diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 7191197c27dbe..f70e4d59796cc 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -27,9 +27,9 @@ Results exceeding `index.max_result_window` are not displayed. * *Show clusters when results exceed 10,000* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents. -* *Use vector tiles.* Vector tiles partition your map into 6 to 8 tiles. +* *Use vector tiles.* Vector tiles partition your map into tiles. Each tile request is limited to the `index.max_result_window` index setting. -Tiles exceeding `index.max_result_window` have a visual indicator when there are too many features to display. +When a tile exceeds `index.max_result_window`, results exceeding `index.max_result_window` are not contained in the tile and a dashed rectangle outlining the bounding box containing all geo values within the tile is displayed. *EMS Boundaries*:: Administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. diff --git a/package.json b/package.json index e6b17783197bc..177b70efd4cc7 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,6 @@ "@mapbox/geojson-rewind": "^0.5.0", "@mapbox/mapbox-gl-draw": "1.3.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", - "@mapbox/vector-tile": "1.3.1", "@reduxjs/toolkit": "^1.6.1", "@slack/webhook": "^5.0.4", "@turf/along": "6.0.1", @@ -460,6 +459,7 @@ "@kbn/test": "link:bazel-bin/packages/kbn-test", "@kbn/test-subj-selector": "link:bazel-bin/packages/kbn-test-subj-selector", "@loaders.gl/polyfills": "^2.3.5", + "@mapbox/vector-tile": "1.3.1", "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@octokit/rest": "^16.35.0", diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index b6b3e636fffeb..42c5b70514000 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -47,14 +47,7 @@ export const CHECK_IS_DRAWING_INDEX = `/${GIS_API_PATH}/checkIsDrawingIndex`; export const MVT_GETTILE_API_PATH = 'mvt/getTile'; export const MVT_GETGRIDTILE_API_PATH = 'mvt/getGridTile'; -export const MVT_SOURCE_LAYER_NAME = 'source_layer'; -// Identifies vector tile "too many features" feature. -// "too many features" feature is a box showing area that contains too many features for single ES search response -export const KBN_METADATA_FEATURE = '__kbn_metadata_feature__'; -export const KBN_FEATURE_COUNT = '__kbn_feature_count__'; -export const KBN_IS_TILE_COMPLETE = '__kbn_is_tile_complete__'; -export const KBN_VECTOR_SHAPE_TYPE_COUNTS = '__kbn_vector_shape_type_counts__'; -export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__'; + // Identifies centroid feature. // Centroids are a single point for representing lines, multiLines, polygons, and multiPolygons export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__'; @@ -119,7 +112,6 @@ export const DEFAULT_MAX_RESULT_WINDOW = 10000; export const DEFAULT_MAX_INNER_RESULT_WINDOW = 100; export const DEFAULT_MAX_BUCKETS_LIMIT = 65535; -export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index 244ebc59efd17..8f681cc9de70d 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -10,21 +10,13 @@ import { Query } from 'src/plugins/data/public'; import { Feature } from 'geojson'; import { - FieldMeta, HeatmapStyleDescriptor, StyleDescriptor, VectorStyleDescriptor, } from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; import { AbstractSourceDescriptor, TermJoinSourceDescriptor } from './source_descriptor_types'; -import { VectorShapeTypeCounts } from '../get_geometry_counts'; -import { - KBN_FEATURE_COUNT, - KBN_IS_TILE_COMPLETE, - KBN_METADATA_FEATURE, - KBN_VECTOR_SHAPE_TYPE_COUNTS, - LAYER_TYPE, -} from '../constants'; +import { LAYER_TYPE } from '../constants'; export type Attribution = { label: string; @@ -38,11 +30,8 @@ export type JoinDescriptor = { export type TileMetaFeature = Feature & { properties: { - [KBN_METADATA_FEATURE]: true; - [KBN_IS_TILE_COMPLETE]: boolean; - [KBN_FEATURE_COUNT]: number; - [KBN_VECTOR_SHAPE_TYPE_COUNTS]: VectorShapeTypeCounts; - fieldMeta?: FieldMeta; + 'hits.total.relation': string; + 'hits.total.value': number; }; }; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/index.ts b/x-pack/plugins/maps/common/elasticsearch_util/index.ts index 7073a4201f7a5..6febb237cdda7 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/index.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/index.ts @@ -6,7 +6,6 @@ */ export * from './es_agg_utils'; -export * from './convert_to_geojson'; export * from './elasticsearch_geo_utils'; export * from './spatial_filter_utils'; export * from './types'; diff --git a/x-pack/plugins/maps/common/get_geometry_counts.ts b/x-pack/plugins/maps/common/get_geometry_counts.ts deleted file mode 100644 index 2a3368560c762..0000000000000 --- a/x-pack/plugins/maps/common/get_geometry_counts.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Feature } from 'geojson'; -import { GEO_JSON_TYPE, VECTOR_SHAPE_TYPE } from './constants'; - -export interface VectorShapeTypeCounts { - [VECTOR_SHAPE_TYPE.POINT]: number; - [VECTOR_SHAPE_TYPE.LINE]: number; - [VECTOR_SHAPE_TYPE.POLYGON]: number; -} - -export function countVectorShapeTypes(features: Feature[]): VectorShapeTypeCounts { - const vectorShapeTypeCounts: VectorShapeTypeCounts = { - [VECTOR_SHAPE_TYPE.POINT]: 0, - [VECTOR_SHAPE_TYPE.LINE]: 0, - [VECTOR_SHAPE_TYPE.POLYGON]: 0, - }; - - for (let i = 0; i < features.length; i++) { - const feature: Feature = features[i]; - if ( - feature.geometry.type === GEO_JSON_TYPE.POINT || - feature.geometry.type === GEO_JSON_TYPE.MULTI_POINT - ) { - vectorShapeTypeCounts[VECTOR_SHAPE_TYPE.POINT] += 1; - } else if ( - feature.geometry.type === GEO_JSON_TYPE.LINE_STRING || - feature.geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING - ) { - vectorShapeTypeCounts[VECTOR_SHAPE_TYPE.LINE] += 1; - } else if ( - feature.geometry.type === GEO_JSON_TYPE.POLYGON || - feature.geometry.type === GEO_JSON_TYPE.MULTI_POLYGON - ) { - vectorShapeTypeCounts[VECTOR_SHAPE_TYPE.POLYGON] += 1; - } - } - - return vectorShapeTypeCounts; -} diff --git a/x-pack/plugins/maps/common/pluck_category_field_meta.ts b/x-pack/plugins/maps/common/pluck_category_field_meta.ts deleted file mode 100644 index c71316f864a84..0000000000000 --- a/x-pack/plugins/maps/common/pluck_category_field_meta.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Feature } from 'geojson'; -import { CategoryFieldMeta } from './descriptor_types'; - -export function pluckCategoryFieldMeta( - features: Feature[], - name: string, - size: number -): CategoryFieldMeta | null { - const counts = new Map(); - for (let i = 0; i < features.length; i++) { - const feature = features[i]; - const term = feature.properties ? feature.properties[name] : undefined; - // properties object may be sparse, so need to check if the field is effectively present - if (typeof term !== undefined) { - if (counts.has(term)) { - counts.set(term, counts.get(term) + 1); - } else { - counts.set(term, 1); - } - } - } - - return trimCategories(counts, size); -} - -export function trimCategories(counts: Map, size: number): CategoryFieldMeta { - const ordered = []; - for (const [key, value] of counts) { - ordered.push({ key, count: value }); - } - - ordered.sort((a, b) => { - return b.count - a.count; - }); - const truncated = ordered.slice(0, size); - return { - categories: truncated, - } as CategoryFieldMeta; -} diff --git a/x-pack/plugins/maps/common/pluck_range_field_meta.ts b/x-pack/plugins/maps/common/pluck_range_field_meta.ts deleted file mode 100644 index b0bf03896892f..0000000000000 --- a/x-pack/plugins/maps/common/pluck_range_field_meta.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Feature } from 'geojson'; -import { RangeFieldMeta } from './descriptor_types'; - -export function pluckRangeFieldMeta( - features: Feature[], - name: string, - parseValue: (rawValue: unknown) => number -): RangeFieldMeta | null { - let min = Infinity; - let max = -Infinity; - for (let i = 0; i < features.length; i++) { - const feature = features[i]; - const newValue = feature.properties ? parseValue(feature.properties[name]) : NaN; - if (!isNaN(newValue)) { - min = Math.min(min, newValue); - max = Math.max(max, newValue); - } - } - - return min === Infinity || max === -Infinity - ? null - : ({ - min, - max, - delta: max - min, - } as RangeFieldMeta); -} diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index b912e8c52e680..c1a6d05cc0577 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -282,10 +282,10 @@ function endDataLoad( if (dataId === SOURCE_DATA_REQUEST_ID) { const features = data && 'features' in data ? (data as FeatureCollection).features : []; + const layer = getLayerById(layerId, getState()); const eventHandlers = getEventHandlers(getState()); if (eventHandlers && eventHandlers.onDataLoadEnd) { - const layer = getLayerById(layerId, getState()); const resultMeta: ResultMeta = {}; if (layer && layer.getType() === LAYER_TYPE.VECTOR) { const featuresWithoutCentroids = features.filter((feature) => { @@ -301,7 +301,9 @@ function endDataLoad( }); } - dispatch(updateTooltipStateForLayer(layerId, features)); + if (layer) { + dispatch(updateTooltipStateForLayer(layer, features)); + } } dispatch({ @@ -344,7 +346,10 @@ function onDataLoadError( }); } - dispatch(updateTooltipStateForLayer(layerId)); + const layer = getLayerById(layerId, getState()); + if (layer) { + dispatch(updateTooltipStateForLayer(layer)); + } } dispatch({ @@ -359,7 +364,10 @@ function onDataLoadError( } export function updateSourceDataRequest(layerId: string, newData: object) { - return (dispatch: ThunkDispatch) => { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { dispatch({ type: UPDATE_SOURCE_DATA_REQUEST, dataId: SOURCE_DATA_REQUEST_ID, @@ -368,7 +376,10 @@ export function updateSourceDataRequest(layerId: string, newData: object) { }); if ('features' in newData) { - dispatch(updateTooltipStateForLayer(layerId, (newData as FeatureCollection).features)); + const layer = getLayerById(layerId, getState()); + if (layer) { + dispatch(updateTooltipStateForLayer(layer, (newData as FeatureCollection).features)); + } } dispatch(updateStyleMeta(layerId)); diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 9e937d86515e2..c9254df40bcf1 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -51,6 +51,7 @@ import { } from '../../common/descriptor_types'; import { ILayer } from '../classes/layers/layer'; import { IVectorLayer } from '../classes/layers/vector_layer'; +import { OnSourceChangeArgs } from '../classes/sources/source'; import { DRAW_MODE, LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants'; import { IVectorStyle } from '../classes/styles/vector/vector_style'; import { notifyLicensedFeatureUsage } from '../licensed_features'; @@ -217,7 +218,7 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) { } if (!makeVisible) { - dispatch(updateTooltipStateForLayer(layerId)); + dispatch(updateTooltipStateForLayer(layer)); } dispatch({ @@ -323,18 +324,17 @@ function updateMetricsProp(layerId: string, value: unknown) { ) => { const layer = getLayerById(layerId, getState()); const previousFields = await (layer as IVectorLayer).getFields(); - await dispatch({ + dispatch({ type: UPDATE_SOURCE_PROP, layerId, propName: 'metrics', value, }); await dispatch(updateStyleProperties(layerId, previousFields as IESAggField[])); - dispatch(syncDataForLayerId(layerId, false)); }; } -export function updateSourceProp( +function updateSourcePropWithoutSync( layerId: string, propName: string, value: unknown, @@ -356,6 +356,28 @@ export function updateSourceProp( if (newLayerType) { dispatch(updateLayerType(layerId, newLayerType)); } + }; +} + +export function updateSourceProp( + layerId: string, + propName: string, + value: unknown, + newLayerType?: LAYER_TYPE +) { + return async (dispatch: ThunkDispatch) => { + await dispatch(updateSourcePropWithoutSync(layerId, propName, value, newLayerType)); + dispatch(syncDataForLayerId(layerId, false)); + }; +} + +export function updateSourceProps(layerId: string, sourcePropChanges: OnSourceChangeArgs[]) { + return async (dispatch: ThunkDispatch) => { + // Using for loop to ensure update completes before starting next update + for (let i = 0; i < sourcePropChanges.length; i++) { + const { propName, value, newLayerType } = sourcePropChanges[i]; + await dispatch(updateSourcePropWithoutSync(layerId, propName, value, newLayerType)); + } dispatch(syncDataForLayerId(layerId, false)); }; } @@ -504,7 +526,7 @@ function removeLayerFromLayerList(layerId: string) { layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); - dispatch(updateTooltipStateForLayer(layerId)); + dispatch(updateTooltipStateForLayer(layerGettingRemoved)); layerGettingRemoved.destroy(); dispatch({ type: REMOVE_LAYER, diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index cf1e22ab90f88..d921f9748f65c 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -63,7 +63,7 @@ import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; import { VectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE } from './ui_actions'; -import { expandToTileBoundaries } from '../../common/geo_tile_utils'; +import { expandToTileBoundaries } from '../classes/util/geo_tile_utils'; import { getToasts } from '../kibana_services'; export function setMapInitError(errorMessage: string) { @@ -171,7 +171,7 @@ export function mapExtentChanged(mapExtentState: MapExtentState) { if (prevZoom !== nextZoom) { getLayerList(getState()).map((layer) => { if (!layer.showAtZoomLevel(nextZoom)) { - dispatch(updateTooltipStateForLayer(layer.getId())); + dispatch(updateTooltipStateForLayer(layer)); } }); } diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index 67b6842caeb46..30213510c8be4 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -10,9 +10,11 @@ import { Dispatch } from 'redux'; import { Feature } from 'geojson'; import { getOpenTooltips } from '../selectors/map_selectors'; import { SET_OPEN_TOOLTIPS } from './map_action_constants'; -import { FEATURE_ID_PROPERTY_NAME, FEATURE_VISIBLE_PROPERTY_NAME } from '../../common/constants'; +import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../common/constants'; import { TooltipFeature, TooltipState } from '../../common/descriptor_types'; import { MapStoreState } from '../reducers/store'; +import { ILayer } from '../classes/layers/layer'; +import { IVectorLayer, getFeatureId, isVectorLayer } from '../classes/layers/vector_layer'; export function closeOnClickTooltip(tooltipId: string) { return (dispatch: Dispatch, getState: () => MapStoreState) => { @@ -62,13 +64,17 @@ export function openOnHoverTooltip(tooltipState: TooltipState) { }; } -export function updateTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) { +export function updateTooltipStateForLayer(layer: ILayer, layerFeatures: Feature[] = []) { return (dispatch: Dispatch, getState: () => MapStoreState) => { + if (!isVectorLayer(layer)) { + return; + } + const openTooltips = getOpenTooltips(getState()) .map((tooltipState) => { const nextFeatures: TooltipFeature[] = []; tooltipState.features.forEach((tooltipFeature) => { - if (tooltipFeature.layerId !== layerId) { + if (tooltipFeature.layerId !== layer.getId()) { // feature from another layer, keep it nextFeatures.push(tooltipFeature); } @@ -79,7 +85,8 @@ export function updateTooltipStateForLayer(layerId: string, layerFeatures: Featu ? layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME] : true; return ( - isVisible && layerFeature.properties![FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id + isVisible && + getFeatureId(layerFeature, (layer as IVectorLayer).getSource()) === tooltipFeature.id ); }); diff --git a/x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts index 70d5cea6e4620..acfe6a9055eb6 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts @@ -34,6 +34,10 @@ export class AggField extends CountAggField { return !!this._esDocField; } + getMbFieldName(): string { + return this._source.isMvt() ? this.getName() + '.value' : this.getName(); + } + supportsFieldMeta(): boolean { // count and sum aggregations are not within field bounds so they do not support field meta. return !isMetricCountable(this._getAggType()); diff --git a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts index 3bd26666005a6..b303dbc342bb2 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts @@ -43,6 +43,10 @@ export class CountAggField implements IESAggField { return this._source.getAggKey(this._getAggType(), this.getRootName()); } + getMbFieldName(): string { + return this._source.isMvt() ? '_count' : this.getName(); + } + getRootName(): string { return ''; } diff --git a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts index f6cdcf43fe343..227084bfe0cad 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts @@ -23,6 +23,10 @@ export class TopTermPercentageField implements IESAggField { return this._topTermAggField.getSource(); } + getMbFieldName(): string { + return this.getName(); + } + getOrigin(): FIELD_ORIGIN { return this._topTermAggField.getOrigin(); } diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts index 586f9f74da8ac..014d75caf90b6 100644 --- a/x-pack/plugins/maps/public/classes/fields/field.ts +++ b/x-pack/plugins/maps/public/classes/fields/field.ts @@ -11,6 +11,7 @@ import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property' export interface IField { getName(): string; + getMbFieldName(): string; getRootName(): string; canValueBeFormatted(): boolean; getLabel(): Promise; @@ -50,6 +51,10 @@ export class AbstractField implements IField { return this._fieldName; } + getMbFieldName(): string { + return this.getName(); + } + getRootName(): string { return this.getName(); } diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index e1043a33f28ad..051115a072608 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -63,13 +63,18 @@ export interface ILayer { getStyleForEditing(): IStyle; getCurrentStyle(): IStyle; getImmutableSourceProperties(): Promise; - renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null; + renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null; isLayerLoading(): boolean; isLoadingBounds(): boolean; isFilteredByGlobalTime(): Promise; hasErrors(): boolean; getErrors(): string; + + /* + * ILayer.getMbLayerIds returns a list of all mapbox layers assoicated with this layer. + */ getMbLayerIds(): string[]; + ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice): void; @@ -77,7 +82,7 @@ export interface ILayer { isInitialDataLoadComplete(): boolean; getIndexPatternIds(): string[]; getQueryableIndexPatternIds(): string[]; - getType(): LAYER_TYPE | undefined; + getType(): LAYER_TYPE; isVisible(): boolean; cloneDescriptor(): Promise; renderStyleEditor( @@ -325,9 +330,8 @@ export class AbstractLayer implements ILayer { return await source.getImmutableProperties(); } - renderSourceSettingsEditor({ onChange }: SourceEditorArgs) { - const source = this.getSourceForEditing(); - return source.renderSourceSettingsEditor({ onChange, currentLayerType: this._descriptor.type }); + renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs) { + return this.getSourceForEditing().renderSourceSettingsEditor(sourceEditorArgs); } getPrevRequestToken(dataId: string): symbol | undefined { @@ -437,7 +441,7 @@ export class AbstractLayer implements ILayer { mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); } - getType(): LAYER_TYPE | undefined { + getType(): LAYER_TYPE { return this._descriptor.type as LAYER_TYPE; } diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap index d3b96936a85a1..8b4911342f841 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap @@ -1,9 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`icon should use no data icon 1`] = ` - `; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx index e1f134cdf2a85..fd78ea2ebde59 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx @@ -95,8 +95,8 @@ describe('visiblity', () => { }); }); -describe('icon', () => { - it('should use no data icon', async () => { +describe('getCustomIconAndTooltipContent', () => { + it('Layers with non-elasticsearch sources should display icon', async () => { const layer: TiledVectorLayer = createLayer({}, {}); const iconAndTooltipContent = layer.getCustomIconAndTooltipContent(); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 9b5298685865a..ece57af7b54ce 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -7,39 +7,44 @@ import type { Map as MbMap, + AnyLayer as MbLayer, GeoJSONSource as MbGeoJSONSource, VectorSource as MbVectorSource, } from '@kbn/mapbox-gl'; import { Feature } from 'geojson'; +import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { parse as parseUrl } from 'url'; -import { i18n } from '@kbn/i18n'; +import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; +import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES } from '../../../../common/constants'; import { - KBN_FEATURE_COUNT, - KBN_IS_TILE_COMPLETE, - KBN_METADATA_FEATURE, - LAYER_TYPE, - SOURCE_DATA_REQUEST_ID, -} from '../../../../common/constants'; -import { + NO_RESULTS_ICON_AND_TOOLTIPCONTENT, VectorLayer, VectorLayerArguments, - NO_RESULTS_ICON_AND_TOOLTIPCONTENT, } from '../vector_layer'; import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; import { DataRequestContext } from '../../../actions'; import { - Timeslice, StyleMetaDescriptor, + TileMetaFeature, + Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, - TileMetaFeature, } from '../../../../common/descriptor_types'; import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types'; +import { ESSearchSource } from '../../sources/es_search_source'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { CustomIconAndTooltipContent } from '../layer'; +const ES_MVT_META_LAYER_NAME = 'meta'; +const ES_MVT_HITS_TOTAL_RELATION = 'hits.total.relation'; +const ES_MVT_HITS_TOTAL_VALUE = 'hits.total.value'; +const MAX_RESULT_WINDOW_DATA_REQUEST_ID = 'maxResultWindow'; + +/* + * MVT vector layer + */ export class TiledVectorLayer extends VectorLayer { static type = LAYER_TYPE.TILED_VECTOR; @@ -70,13 +75,46 @@ export class TiledVectorLayer extends VectorLayer { } getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { - const tileMetas = this._getMetaFromTiles(); - if (!tileMetas.length) { + const icon = this.getCurrentStyle().getIcon(); + if (!this.getSource().isESSource()) { + // Only ES-sources can have a special meta-tile, not 3rd party vector tile sources + return { + icon, + tooltipContent: null, + areResultsTrimmed: false, + }; + } + + // + // TODO ES MVT specific - move to es_tiled_vector_layer implementation + // + + const tileMetaFeatures = this._getMetaFromTiles(); + if (!tileMetaFeatures.length) { return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; } - const totalFeaturesCount: number = tileMetas.reduce((acc: number, tileMeta: Feature) => { - const count = tileMeta && tileMeta.properties ? tileMeta.properties[KBN_FEATURE_COUNT] : 0; + if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) { + // aggregation ES sources are never trimmed + return { + icon, + tooltipContent: null, + areResultsTrimmed: false, + }; + } + + const maxResultWindow = this._getMaxResultWindow(); + if (maxResultWindow === undefined) { + return { + icon, + tooltipContent: null, + areResultsTrimmed: false, + }; + } + + const totalFeaturesCount: number = tileMetaFeatures.reduce((acc: number, tileMeta: Feature) => { + const count = + tileMeta && tileMeta.properties ? tileMeta.properties[ES_MVT_HITS_TOTAL_VALUE] : 0; return count + acc; }, 0); @@ -84,12 +122,16 @@ export class TiledVectorLayer extends VectorLayer { return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; } - const isIncomplete: boolean = tileMetas.some((tileMeta: Feature) => { - return !tileMeta?.properties?.[KBN_IS_TILE_COMPLETE]; + const isIncomplete: boolean = tileMetaFeatures.some((tileMeta: TileMetaFeature) => { + if (tileMeta?.properties?.[ES_MVT_HITS_TOTAL_RELATION] === 'gte') { + return tileMeta?.properties?.[ES_MVT_HITS_TOTAL_VALUE] >= maxResultWindow + 1; + } else { + return false; + } }); return { - icon: this.getCurrentStyle().getIcon(), + icon, tooltipContent: isIncomplete ? i18n.translate('xpack.maps.tiles.resultsTrimmedMsg', { defaultMessage: `Results limited to {count} documents.`, @@ -107,6 +149,27 @@ export class TiledVectorLayer extends VectorLayer { }; } + _getMaxResultWindow(): number | undefined { + const dataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID); + if (!dataRequest) { + return; + } + const data = dataRequest.getData() as { maxResultWindow: number } | undefined; + return data ? data.maxResultWindow : undefined; + } + + async _syncMaxResultWindow({ startLoading, stopLoading }: DataRequestContext) { + const prevDataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID); + if (prevDataRequest) { + return; + } + + const requestToken = Symbol(`${this.getId()}-${MAX_RESULT_WINDOW_DATA_REQUEST_ID}`); + startLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken); + const maxResultWindow = await (this.getSource() as ESSearchSource).getMaxResultWindow(); + stopLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken, { maxResultWindow }); + } + async _syncMVTUrlTemplate({ startLoading, stopLoading, @@ -141,6 +204,7 @@ export class TiledVectorLayer extends VectorLayer { }, }); const canSkip = noChangesInSourceState && noChangesInSearchState; + if (canSkip) { return null; } @@ -180,6 +244,9 @@ export class TiledVectorLayer extends VectorLayer { } async syncData(syncContext: DataRequestContext) { + if (this.getSource().getType() === SOURCE_TYPES.ES_SEARCH) { + await this._syncMaxResultWindow(syncContext); + } await this._syncSourceStyleMeta(syncContext, this._source, this._style as IVectorStyle); await this._syncSourceFormatters(syncContext, this._source, this._style as IVectorStyle); await this._syncMVTUrlTemplate(syncContext); @@ -213,10 +280,18 @@ export class TiledVectorLayer extends VectorLayer { }); } + getMbLayerIds() { + return [...super.getMbLayerIds(), this._getMbTooManyFeaturesLayerId()]; + } + ownsMbSourceId(mbSourceId: string): boolean { return this._getMbSourceId() === mbSourceId; } + _getMbTooManyFeaturesLayerId() { + return this.makeMbLayerId('toomanyfeatures'); + } + _syncStylePropertiesWithMb(mbMap: MbMap) { // @ts-ignore const mbSource = mbMap.getSource(this._getMbSourceId()); @@ -236,10 +311,52 @@ export class TiledVectorLayer extends VectorLayer { this._setMbPointsProperties(mbMap, sourceMeta.layerName); this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); - this._setMbCentroidProperties(mbMap, sourceMeta.layerName); + this._setMbLabelProperties(mbMap, sourceMeta.layerName); + this._syncTooManyFeaturesProperties(mbMap); + } + + // TODO ES MVT specific - move to es_tiled_vector_layer implementation + _syncTooManyFeaturesProperties(mbMap: MbMap) { + if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) { + return; + } + + const maxResultWindow = this._getMaxResultWindow(); + if (maxResultWindow === undefined) { + return; + } + + const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId(); + + if (!mbMap.getLayer(tooManyFeaturesLayerId)) { + const mbTooManyFeaturesLayer: MbLayer = { + id: tooManyFeaturesLayerId, + type: 'line', + source: this.getId(), + paint: {}, + }; + mbTooManyFeaturesLayer['source-layer'] = ES_MVT_META_LAYER_NAME; + mbMap.addLayer(mbTooManyFeaturesLayer); + mbMap.setFilter(tooManyFeaturesLayerId, [ + 'all', + ['==', ['get', ES_MVT_HITS_TOTAL_RELATION], 'gte'], + ['>=', ['get', ES_MVT_HITS_TOTAL_VALUE], maxResultWindow + 1], + ]); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-color', euiThemeVars.euiColorWarning); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-width', 3); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-dasharray', [2, 1]); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-opacity', this.getAlpha()); + } + + this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId); + mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } queryTileMetaFeatures(mbMap: MbMap): TileMetaFeature[] | null { + if (!this.getSource().isESSource()) { + return null; + } + // @ts-ignore const mbSource = mbMap.getSource(this._getMbSourceId()); if (!mbSource) { @@ -259,26 +376,38 @@ export class TiledVectorLayer extends VectorLayer { // querySourceFeatures can return duplicated features when features cross tile boundaries. // Tile meta will never have duplicated features since by there nature, tile meta is a feature contained within a single tile const mbFeatures = mbMap.querySourceFeatures(this._getMbSourceId(), { - sourceLayer: sourceMeta.layerName, - filter: ['==', ['get', KBN_METADATA_FEATURE], true], + sourceLayer: ES_MVT_META_LAYER_NAME, }); - const metaFeatures: TileMetaFeature[] = mbFeatures.map((mbFeature: Feature) => { + const metaFeatures: Array = ( + mbFeatures as unknown as TileMetaFeature[] + ).map((mbFeature: TileMetaFeature | null) => { const parsedProperties: Record = {}; - for (const key in mbFeature.properties) { - if (mbFeature.properties.hasOwnProperty(key)) { - parsedProperties[key] = JSON.parse(mbFeature.properties[key]); // mvt properties cannot be nested geojson + for (const key in mbFeature?.properties) { + if (mbFeature?.properties.hasOwnProperty(key)) { + parsedProperties[key] = + typeof mbFeature.properties[key] === 'string' || + typeof mbFeature.properties[key] === 'number' || + typeof mbFeature.properties[key] === 'boolean' + ? mbFeature.properties[key] + : JSON.parse(mbFeature.properties[key]); // mvt properties cannot be nested geojson } } - return { - type: 'Feature', - id: mbFeature.id, - geometry: mbFeature.geometry, - properties: parsedProperties, - } as TileMetaFeature; + + try { + return { + type: 'Feature', + id: mbFeature?.id, + geometry: mbFeature?.geometry, // this getter might throw with non-conforming geometries + properties: parsedProperties, + } as TileMetaFeature; + } catch (e) { + return null; + } }); - return metaFeatures as TileMetaFeature[]; + const filtered = metaFeatures.filter((f) => f !== null); + return filtered as TileMetaFeature[]; } _requiresPrevSourceCleanup(mbMap: MbMap): boolean { @@ -317,8 +446,13 @@ export class TiledVectorLayer extends VectorLayer { const mbLayer = mbMap.getLayer(layerIds[i]); // The mapbox type in the spec is specified with `source-layer` // but the programmable JS-object uses camelcase `sourceLayer` - // @ts-expect-error - if (mbLayer && mbLayer.sourceLayer !== tiledSourceMeta.layerName) { + if ( + mbLayer && + // @ts-expect-error + mbLayer.sourceLayer !== tiledSourceMeta.layerName && + // @ts-expect-error + mbLayer.sourceLayer !== ES_MVT_META_LAYER_NAME + ) { // If the source-pointer of one of the layers is stale, they will all be stale. // In this case, all the mb-layers need to be removed and re-added. return true; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts index 137d443b39b91..2250e86da0ec2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { assignFeatureIds } from './assign_feature_ids'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { assignFeatureIds, GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; import { FeatureCollection, Feature, Point } from 'geojson'; const featureId = 'myFeature1'; @@ -34,7 +33,7 @@ test('should provide unique id when feature.id is not provided', () => { expect(typeof feature1.id).toBe('number'); expect(typeof feature2.id).toBe('number'); // @ts-ignore - expect(feature1.id).toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.id).toBe(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]); expect(feature1.id).not.toBe(feature2.id); }); @@ -53,9 +52,9 @@ test('should preserve feature id when provided', () => { const feature1 = updatedFeatureCollection.features[0]; expect(typeof feature1.id).toBe('number'); // @ts-ignore - expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.id).not.toBe(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]); // @ts-ignore - expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); + expect(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]).toBe(featureId); }); test('should preserve feature id for falsy value', () => { @@ -73,9 +72,9 @@ test('should preserve feature id for falsy value', () => { const feature1 = updatedFeatureCollection.features[0]; expect(typeof feature1.id).toBe('number'); // @ts-ignore - expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.id).not.toBe(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]); // @ts-ignore - expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(0); + expect(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]).toBe(0); }); test('should not modify original feature properties', () => { @@ -94,6 +93,6 @@ test('should not modify original feature properties', () => { const updatedFeatureCollection = assignFeatureIds(featureCollection); const feature1 = updatedFeatureCollection.features[0]; // @ts-ignore - expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); - expect(featureProperties).not.toHaveProperty(FEATURE_ID_PROPERTY_NAME); + expect(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]).toBe(featureId); + expect(featureProperties).not.toHaveProperty(GEOJSON_FEATURE_ID_PROPERTY_NAME); }); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts index c40c8299ad04c..53ce15439e815 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts @@ -7,7 +7,11 @@ import _ from 'lodash'; import { FeatureCollection, Feature } from 'geojson'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { SOURCE_TYPES } from '../../../../common/constants'; +import { IVectorSource } from '../../sources/vector_source'; + +export const GEOJSON_FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; +export const ES_MVT_FEATURE_ID_PROPERTY_NAME = '_id'; let idCounter = 0; @@ -43,7 +47,7 @@ export function assignFeatureIds(featureCollection: FeatureCollection): FeatureC geometry: feature.geometry, // do not copy geometry, this object can be massive properties: { // preserve feature id provided by source so features can be referenced across fetches - [FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id, + [GEOJSON_FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id, // create new object for properties so original is not polluted with kibana internal props ...feature.properties, }, @@ -56,3 +60,13 @@ export function assignFeatureIds(featureCollection: FeatureCollection): FeatureC features, }; } + +export function getFeatureId(feature: Feature, source: IVectorSource): string | number | undefined { + if (!source.isMvt()) { + return feature.properties?.[GEOJSON_FEATURE_ID_PROPERTY_NAME]; + } + + return source.getType() === SOURCE_TYPES.ES_SEARCH + ? feature.properties?.[ES_MVT_FEATURE_ID_PROPERTY_NAME] + : feature.id; +} diff --git a/x-pack/plugins/maps/common/get_centroid_features.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.test.ts similarity index 93% rename from x-pack/plugins/maps/common/get_centroid_features.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.test.ts index 0fac9dc3a355f..2b8d03a2c3f8c 100644 --- a/x-pack/plugins/maps/common/get_centroid_features.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.test.ts @@ -44,35 +44,6 @@ test('should not create centroid feature for point and multipoint', () => { expect(centroidFeatures.length).toBe(0); }); -test('should not create centroid for the metadata polygon', () => { - const polygonFeature: Feature = { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [ - [ - [35, 10], - [45, 45], - [15, 40], - [10, 20], - [35, 10], - ], - ], - }, - properties: { - __kbn_metadata_feature__: true, - prop0: 'value0', - prop1: 0.0, - }, - }; - const featureCollection: FeatureCollection = { - type: 'FeatureCollection', - features: [polygonFeature], - }; - const centroidFeatures = getCentroidFeatures(featureCollection); - expect(centroidFeatures.length).toBe(0); -}); - test('should create centroid feature for line (even number of points)', () => { const lineFeature: Feature = { type: 'Feature', diff --git a/x-pack/plugins/maps/common/get_centroid_features.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts similarity index 94% rename from x-pack/plugins/maps/common/get_centroid_features.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts index 8aaeb56576a84..6afe61f8a16b9 100644 --- a/x-pack/plugins/maps/common/get_centroid_features.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts @@ -21,18 +21,13 @@ import turfArea from '@turf/area'; import turfCenterOfMass from '@turf/center-of-mass'; import turfLength from '@turf/length'; import { lineString, polygon } from '@turf/helpers'; -import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE, KBN_METADATA_FEATURE } from './constants'; +import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE } from '../../../../common/constants'; export function getCentroidFeatures(featureCollection: FeatureCollection): Feature[] { const centroids = []; for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; - // do not add centroid for kibana added features - if (feature.properties?.[KBN_METADATA_FEATURE]) { - continue; - } - const centroid = getCentroid(feature); if (centroid) { centroids.push(centroid); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts index cb964f77613da..80d83996d8fd6 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts @@ -13,3 +13,4 @@ export { VectorLayerArguments, NO_RESULTS_ICON_AND_TOOLTIPCONTENT, } from './vector_layer'; +export { getFeatureId } from './assign_feature_ids'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx index 8e4eb349036ea..cc30f30fe9898 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx @@ -24,7 +24,7 @@ import { DataRequestContext } from '../../../actions'; import { IVectorSource } from '../../sources/vector_source'; import { DataRequestAbortError } from '../../util/data_request'; import { DataRequest } from '../../util/data_request'; -import { getCentroidFeatures } from '../../../../common/get_centroid_features'; +import { getCentroidFeatures } from './get_centroid_features'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { assignFeatureIds } from './assign_feature_ids'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 3faf92715451c..cd1b644e9cfba 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -21,20 +21,16 @@ import { AbstractLayer } from '../layer'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; import { AGG_TYPE, - FEATURE_ID_PROPERTY_NAME, SOURCE_META_DATA_REQUEST_ID, SOURCE_FORMATTERS_DATA_REQUEST_ID, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, - KBN_METADATA_FEATURE, LAYER_TYPE, FIELD_ORIGIN, - KBN_TOO_MANY_FEATURES_IMAGE_ID, FieldFormatter, SOURCE_TYPES, STYLE_TYPE, SUPPORTS_FEATURE_EDITING_REQUEST_ID, - KBN_IS_TILE_COMPLETE, VECTOR_STYLES, } from '../../../../common/constants'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; @@ -46,7 +42,7 @@ import { } from '../../util/can_skip_fetch'; import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds'; import { - getCentroidFilterExpression, + getLabelFilterExpression, getFillFilterExpression, getLineFilterExpression, getPointFilterExpression, @@ -80,6 +76,7 @@ import { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './u import { JoinState, performInnerJoins } from './perform_inner_joins'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { getJoinAggKey } from '../../../../common/get_agg_key'; +import { getFeatureId } from './assign_feature_ids'; export function isVectorLayer(layer: ILayer) { return (layer as IVectorLayer).canShowTooltip !== undefined; @@ -93,6 +90,12 @@ export interface VectorLayerArguments { } export interface IVectorLayer extends ILayer { + /* + * IVectorLayer.getMbLayerIds returns a list of mapbox layers assoicated with this layer for identifing features with tooltips. + * Must return ILayer.getMbLayerIds or a subset of ILayer.getMbLayerIds. + */ + getMbTooltipLayerIds(): string[]; + getFields(): Promise; getStyleEditorFields(): Promise; getJoins(): InnerJoin[]; @@ -118,6 +121,9 @@ export const NO_RESULTS_ICON_AND_TOOLTIPCONTENT = { }), }; +/* + * Geojson vector layer + */ export class VectorLayer extends AbstractLayer implements IVectorLayer { static type = LAYER_TYPE.VECTOR; @@ -589,6 +595,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { timeFilters: nextMeta.timeFilters, searchSessionId: dataFilters.searchSessionId, }); + stopLoading(dataRequestId, requestToken, styleMeta, nextMeta); } catch (error) { if (!(error instanceof DataRequestAbortError)) { @@ -774,6 +781,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } _getSourceFeatureCollection() { + if (this.getSource().isMvt()) { + return null; + } const sourceDataRequest = this.getSourceDataRequest(); return sourceDataRequest ? (sourceDataRequest.getData() as FeatureCollection) : null; } @@ -946,7 +956,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); - const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId(); const hasJoins = this.hasJoins(); if (!mbMap.getLayer(fillLayerId)) { @@ -973,29 +982,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } mbMap.addLayer(mbLayer); } - if (!mbMap.getLayer(tooManyFeaturesLayerId)) { - const mbLayer: MbLayer = { - id: tooManyFeaturesLayerId, - type: 'fill', - source: sourceId, - paint: {}, - }; - if (mvtSourceLayer) { - mbLayer['source-layer'] = mvtSourceLayer; - } - mbMap.addLayer(mbLayer); - mbMap.setFilter(tooManyFeaturesLayerId, [ - 'all', - ['==', ['get', KBN_METADATA_FEATURE], true], - ['==', ['get', KBN_IS_TILE_COMPLETE], false], - ]); - mbMap.setPaintProperty( - tooManyFeaturesLayerId, - 'fill-pattern', - KBN_TOO_MANY_FEATURES_IMAGE_ID - ); - mbMap.setPaintProperty(tooManyFeaturesLayerId, 'fill-opacity', this.getAlpha()); - } this.getCurrentStyle().setMBPaintProperties({ alpha: this.getAlpha(), @@ -1017,21 +1003,18 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) { mbMap.setFilter(lineLayerId, lineFilterExpr); } - - this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId); - mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } - _setMbCentroidProperties( + _setMbLabelProperties( mbMap: MbMap, mvtSourceLayer?: string, timesliceMaskConfig?: TimesliceMaskConfig ) { - const centroidLayerId = this._getMbCentroidLayerId(); - const centroidLayer = mbMap.getLayer(centroidLayerId); - if (!centroidLayer) { + const labelLayerId = this._getMbLabelLayerId(); + const labelLayer = mbMap.getLayer(labelLayerId); + if (!labelLayer) { const mbLayer: MbLayer = { - id: centroidLayerId, + id: labelLayerId, type: 'symbol', source: this.getId(), }; @@ -1041,27 +1024,32 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getCentroidFilterExpression(this.hasJoins(), timesliceMaskConfig); - if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) { - mbMap.setFilter(centroidLayerId, filterExpr); + const isSourceGeoJson = !this.getSource().isMvt(); + const filterExpr = getLabelFilterExpression( + this.hasJoins(), + isSourceGeoJson, + timesliceMaskConfig + ); + if (!_.isEqual(filterExpr, mbMap.getFilter(labelLayerId))) { + mbMap.setFilter(labelLayerId, filterExpr); } this.getCurrentStyle().setMBPropertiesForLabelText({ alpha: this.getAlpha(), mbMap, - textLayerId: centroidLayerId, + textLayerId: labelLayerId, }); - this.syncVisibilityWithMb(mbMap, centroidLayerId); - mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom()); + this.syncVisibilityWithMb(mbMap, labelLayerId); + mbMap.setLayerZoomRange(labelLayerId, this.getMinZoom(), this.getMaxZoom()); } _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) { const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); - // centroid layers added after polygon layers to ensure they are on top of polygon layers - this._setMbCentroidProperties(mbMap, undefined, timesliceMaskConfig); + // label layers added after geometry layers to ensure they are on top + this._setMbLabelProperties(mbMap, undefined, timesliceMaskConfig); } _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { @@ -1092,8 +1080,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return this.makeMbLayerId('text'); } - _getMbCentroidLayerId() { - return this.makeMbLayerId('centroid'); + // _getMbTextLayerId is labels for Points and MultiPoints + // _getMbLabelLayerId is labels for not Points and MultiPoints + // _getMbLabelLayerId used to be called _getMbCentroidLayerId + // TODO merge textLayer and labelLayer into single layer + _getMbLabelLayerId() { + return this.makeMbLayerId('label'); } _getMbSymbolLayerId() { @@ -1108,22 +1100,21 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return this.makeMbLayerId('fill'); } - _getMbTooManyFeaturesLayerId() { - return this.makeMbLayerId('toomanyfeatures'); - } - - getMbLayerIds() { + getMbTooltipLayerIds() { return [ this._getMbPointLayerId(), this._getMbTextLayerId(), - this._getMbCentroidLayerId(), + this._getMbLabelLayerId(), this._getMbSymbolLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId(), - this._getMbTooManyFeaturesLayerId(), ]; } + getMbLayerIds() { + return this.getMbTooltipLayerIds(); + } + ownsMbLayerId(mbLayerId: string) { return this.getMbLayerIds().includes(mbLayerId); } @@ -1170,7 +1161,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } const targetFeature = featureCollection.features.find((feature) => { - return feature.properties?.[FEATURE_ID_PROPERTY_NAME] === id; + return getFeatureId(feature, this.getSource()) === id; }); return targetFeature ? targetFeature : null; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts index 78bc2592182d8..dc9637c7a7637 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts @@ -134,14 +134,14 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE return valueAggsDsl; } - async getTooltipProperties(properties: GeoJsonProperties): Promise { + async getTooltipProperties(mbProperties: GeoJsonProperties): Promise { const metricFields = await this.getFields(); const promises: Array> = []; metricFields.forEach((metricField) => { let value; - for (const key in properties) { - if (properties.hasOwnProperty(key) && metricField.getName() === key) { - value = properties[key]; + for (const key in mbProperties) { + if (mbProperties.hasOwnProperty(key) && metricField.getMbFieldName() === key) { + value = mbProperties[key]; break; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap index ca9775594a9d7..6a1dbf9e1590b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap @@ -1,73 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`resolution editor should add super-fine option 1`] = ` - - - + + + + + `; exports[`resolution editor should omit super-fine option 1`] = ` - - - + + + + + `; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap index dfce6b36396a7..0d4467792e636 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap @@ -19,9 +19,9 @@ exports[`source editor geo_grid_source default vector layer config should allow /> @@ -45,6 +45,7 @@ exports[`source editor geo_grid_source default vector layer config should allow /> @@ -79,7 +80,8 @@ exports[`source editor geo_grid_source should put limitations based on heatmap-r /> diff --git a/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.d.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.d.ts similarity index 89% rename from x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.d.ts rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.d.ts index 3bb2e165fa740..9452620447259 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.d.ts @@ -6,7 +6,7 @@ */ import { Feature } from 'geojson'; -import { RENDER_AS } from '../constants'; +import { RENDER_AS } from '../../../../common/constants'; export function convertCompositeRespToGeoJson(esResponse: any, renderAs: RENDER_AS): Feature[]; export function convertRegularRespToGeoJson(esResponse: any, renderAs: RENDER_AS): Feature[]; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js similarity index 89% rename from x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js index f416847941a8a..87fb0d53efa62 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js @@ -6,10 +6,13 @@ */ import _ from 'lodash'; -import { RENDER_AS, GEOTILE_GRID_AGG_NAME, GEOCENTROID_AGG_NAME } from '../constants'; -import { getTileBoundingBox } from '../geo_tile_utils'; -import { extractPropertiesFromBucket } from './es_agg_utils'; -import { clamp } from './elasticsearch_geo_utils'; +import { + RENDER_AS, + GEOTILE_GRID_AGG_NAME, + GEOCENTROID_AGG_NAME, +} from '../../../../common/constants'; +import { getTileBoundingBox } from '../../util/geo_tile_utils'; +import { clamp, extractPropertiesFromBucket } from '../../../../common/elasticsearch_util'; const GRID_BUCKET_KEYS_TO_IGNORE = ['key', GEOCENTROID_AGG_NAME]; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts similarity index 98% rename from x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.test.ts rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts index 2dc96038d931d..d6de17bef710b 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts @@ -7,7 +7,7 @@ // @ts-ignore import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; -import { RENDER_AS } from '../constants'; +import { RENDER_AS } from '../../../../common/constants'; describe('convertCompositeRespToGeoJson', () => { const esResponse = { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 41d5715e47b8e..8eebf01a550dd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -296,7 +296,7 @@ describe('ESGeoGridSource', () => { ); it('getLayerName', () => { - expect(mvtGeogridSource.getLayerName()).toBe('source_layer'); + expect(mvtGeogridSource.getLayerName()).toBe('aggs'); }); it('getMinZoom', () => { @@ -312,28 +312,13 @@ describe('ESGeoGridSource', () => { vectorSourceRequestMeta ); - expect(urlTemplateWithMeta.layerName).toBe('source_layer'); + expect(urlTemplateWithMeta.layerName).toBe('aggs'); expect(urlTemplateWithMeta.minSourceZoom).toBe(0); expect(urlTemplateWithMeta.maxSourceZoom).toBe(24); expect(urlTemplateWithMeta.urlTemplate).toEqual( - "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point" + "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':())))&requestType=heatmap" ); }); - - it('should include searchSourceId in urlTemplateWithMeta', async () => { - const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta({ - ...vectorSourceRequestMeta, - searchSessionId: '1', - }); - - expect( - urlTemplateWithMeta.urlTemplate.startsWith( - "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1" - ) - ).toBe(true); - - expect(urlTemplateWithMeta.urlTemplate.endsWith('&searchSessionId=1')).toBe(true); - }); }); describe('Gold+ usage', () => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 7bd1e4dfd75f3..d038c139a1667 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -11,12 +11,8 @@ import { i18n } from '@kbn/i18n'; import rison from 'rison-node'; import { Feature } from 'geojson'; import type { estypes } from '@elastic/elasticsearch'; -import { - convertCompositeRespToGeoJson, - convertRegularRespToGeoJson, - makeESBbox, -} from '../../../../common/elasticsearch_util'; -// @ts-expect-error +import { makeESBbox } from '../../../../common/elasticsearch_util'; +import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; import { UpdateSourceEditor } from './update_source_editor'; import { DEFAULT_MAX_BUCKETS_LIMIT, @@ -26,7 +22,6 @@ import { GIS_API_PATH, GRID_RESOLUTION, MVT_GETGRIDTILE_API_PATH, - MVT_SOURCE_LAYER_NAME, MVT_TOKEN_PARAM_NAME, RENDER_AS, SOURCE_TYPES, @@ -55,6 +50,8 @@ import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/ type ESGeoGridSourceSyncMeta = Pick; +const ES_MVT_AGGS_LAYER_NAME = 'aggs'; + export const MAX_GEOTILE_LEVEL = 29; export const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', { @@ -140,6 +137,10 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle ]; } + isMvt() { + return this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE; + } + getFieldNames() { return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName()); } @@ -305,8 +306,8 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle _addNonCompositeAggsToSearchSource( searchSource: ISearchSource, indexPattern: IndexPattern, - precision: number | null, - bufferedExtent?: MapExtent | null + precision: number, + bufferedExtent?: MapExtent ) { searchSource.setField('aggs', { [GEOTILE_GRID_AGG_NAME]: { @@ -419,7 +420,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle } getLayerName(): string { - return MVT_SOURCE_LAYER_NAME; + return ES_MVT_AGGS_LAYER_NAME; } async getUrlTemplateWithMeta( @@ -427,14 +428,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle ): Promise { const indexPattern = await this.getIndexPattern(); const searchSource = await this.makeSearchSource(searchFilters, 0); - - this._addNonCompositeAggsToSearchSource( - searchSource, - indexPattern, - null, // needs to be set server-side - null // needs to be stripped server-side - ); - + searchSource.setField('aggs', this.getValueAggsDsl(indexPattern)); const dsl = searchSource.getSearchRequestBody(); const risonDsl = rison.encode(dsl); @@ -443,22 +437,18 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle `/${GIS_API_PATH}/${MVT_GETGRIDTILE_API_PATH}/{z}/{x}/{y}.pbf` ); - const geoField = await this._getGeoField(); const urlTemplate = `${mvtUrlServicePath}\ ?geometryFieldName=${this._descriptor.geoField}\ &index=${indexPattern.title}\ &requestBody=${risonDsl}\ -&requestType=${this._descriptor.requestType}\ -&geoFieldType=${geoField.type}`; +&requestType=${this._descriptor.requestType}`; return { refreshTokenParamName: MVT_TOKEN_PARAM_NAME, layerName: this.getLayerName(), minSourceZoom: this.getMinZoom(), maxSourceZoom: this.getMaxZoom(), - urlTemplate: searchFilters.searchSessionId - ? urlTemplate + `&searchSessionId=${searchFilters.searchSessionId}` - : urlTemplate, + urlTemplate, }; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js deleted file mode 100644 index 52f4e4c9b7b88..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { GRID_RESOLUTION } from '../../../../common/constants'; -import { EuiSelect, EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -const BASE_OPTIONS = [ - { - value: GRID_RESOLUTION.COARSE, - text: i18n.translate('xpack.maps.source.esGrid.coarseDropdownOption', { - defaultMessage: 'coarse', - }), - }, - { - value: GRID_RESOLUTION.FINE, - text: i18n.translate('xpack.maps.source.esGrid.fineDropdownOption', { - defaultMessage: 'fine', - }), - }, - { - value: GRID_RESOLUTION.MOST_FINE, - text: i18n.translate('xpack.maps.source.esGrid.finestDropdownOption', { - defaultMessage: 'finest', - }), - }, -]; - -export function ResolutionEditor({ resolution, onChange, includeSuperFine }) { - const options = [...BASE_OPTIONS]; - - if (includeSuperFine) { - options.push({ - value: GRID_RESOLUTION.SUPER_FINE, - text: i18n.translate('xpack.maps.source.esGrid.superFineDropDownOption', { - defaultMessage: 'super fine (beta)', - }), - }); - } - - return ( - - onChange(e.target.value)} - compressed - /> - - ); -} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx index 0066160402fa3..a642bbe41449f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -// @ts-expect-error import { ResolutionEditor } from './resolution_editor'; import { GRID_RESOLUTION } from '../../../../common/constants'; @@ -16,6 +15,7 @@ const defaultProps = { resolution: GRID_RESOLUTION.COARSE, onChange: () => {}, includeSuperFine: false, + metrics: [], }; describe('resolution editor', () => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx new file mode 100644 index 0000000000000..55ce46e121273 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ChangeEvent, Component } from 'react'; +import { EuiConfirmModal, EuiSelect, EuiFormRow } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { AggDescriptor } from '../../../../common/descriptor_types'; +import { AGG_TYPE, GRID_RESOLUTION } from '../../../../common/constants'; + +const BASE_OPTIONS = [ + { + value: GRID_RESOLUTION.COARSE, + text: i18n.translate('xpack.maps.source.esGrid.coarseDropdownOption', { + defaultMessage: 'coarse', + }), + }, + { + value: GRID_RESOLUTION.FINE, + text: i18n.translate('xpack.maps.source.esGrid.fineDropdownOption', { + defaultMessage: 'fine', + }), + }, + { + value: GRID_RESOLUTION.MOST_FINE, + text: i18n.translate('xpack.maps.source.esGrid.finestDropdownOption', { + defaultMessage: 'finest', + }), + }, +]; + +function isUnsupportedVectorTileMetric(metric: AggDescriptor) { + return metric.type === AGG_TYPE.TERMS; +} + +interface Props { + includeSuperFine: boolean; + resolution: GRID_RESOLUTION; + onChange: (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => void; + metrics: AggDescriptor[]; +} + +interface State { + showModal: boolean; +} + +export class ResolutionEditor extends Component { + private readonly _options = [...BASE_OPTIONS]; + + constructor(props: Props) { + super(props); + + this.state = { + showModal: false, + }; + + if (props.includeSuperFine) { + this._options.push({ + value: GRID_RESOLUTION.SUPER_FINE, + text: i18n.translate('xpack.maps.source.esGrid.superFineDropDownOption', { + defaultMessage: 'super fine', + }), + }); + } + } + + _onResolutionChange = (e: ChangeEvent) => { + const resolution = e.target.value as GRID_RESOLUTION; + if (resolution === GRID_RESOLUTION.SUPER_FINE) { + const hasUnsupportedMetrics = this.props.metrics.find(isUnsupportedVectorTileMetric); + if (hasUnsupportedMetrics) { + this.setState({ showModal: true }); + return; + } + } + + this.props.onChange(resolution, this.props.metrics); + }; + + _closeModal = () => { + this.setState({ + showModal: false, + }); + }; + + _acceptModal = () => { + this._closeModal(); + const supportedMetrics = this.props.metrics.filter((metric) => { + return !isUnsupportedVectorTileMetric(metric); + }); + this.props.onChange( + GRID_RESOLUTION.SUPER_FINE, + supportedMetrics.length ? supportedMetrics : [{ type: AGG_TYPE.COUNT }] + ); + }; + + _renderModal() { + return this.state.showModal ? ( + +

+ +

+
+ ) : null; + } + + render() { + const helpText = + this.props.resolution === GRID_RESOLUTION.SUPER_FINE + ? i18n.translate('xpack.maps.source.esGrid.superFineHelpText', { + defaultMessage: 'Super fine grid resolution uses vector tiles.', + }) + : undefined; + return ( + <> + {this._renderModal()} + + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx index 18a263143afea..9dab6698b73f0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx @@ -8,14 +8,19 @@ import React from 'react'; import { shallow } from 'enzyme'; -// @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; import { GRID_RESOLUTION, LAYER_TYPE, RENDER_AS } from '../../../../common/constants'; +jest.mock('uuid/v4', () => { + return function () { + return '12345'; + }; +}); + const defaultProps = { currentLayerType: LAYER_TYPE.VECTOR, indexPatternId: 'foobar', - onChange: () => {}, + onChange: async () => {}, metrics: [], renderAs: RENDER_AS.POINT, resolution: GRID_RESOLUTION.COARSE, diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx similarity index 61% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx index c2a0935c23719..03b5e1fe3c794 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx @@ -7,20 +7,40 @@ import React, { Fragment, Component } from 'react'; +import uuid from 'uuid/v4'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPanel, EuiSpacer, EuiComboBoxOptionOption, EuiTitle } from '@elastic/eui'; import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters'; -import { GRID_RESOLUTION, LAYER_TYPE } from '../../../../common/constants'; +import { AGG_TYPE, GRID_RESOLUTION, LAYER_TYPE, RENDER_AS } from '../../../../common/constants'; import { MetricsEditor } from '../../../components/metrics_editor'; import { getIndexPatternService } from '../../../kibana_services'; import { ResolutionEditor } from './resolution_editor'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { isMetricCountable } from '../../util/is_metric_countable'; -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; +import { IndexPatternField, indexPatterns } from '../../../../../../../src/plugins/data/public'; import { RenderAsSelect } from './render_as_select'; +import { AggDescriptor } from '../../../../common/descriptor_types'; +import { OnSourceChangeArgs } from '../source'; + +interface Props { + currentLayerType?: string; + indexPatternId: string; + onChange: (...args: OnSourceChangeArgs[]) => Promise; + metrics: AggDescriptor[]; + renderAs: RENDER_AS; + resolution: GRID_RESOLUTION; +} -export class UpdateSourceEditor extends Component { - state = { - fields: null, +interface State { + metricsEditorKey: string; + fields: IndexPatternField[]; + loadError?: string; +} + +export class UpdateSourceEditor extends Component { + private _isMounted?: boolean; + state: State = { + fields: [], + metricsEditorKey: uuid(), }; componentDidMount() { @@ -54,11 +74,11 @@ export class UpdateSourceEditor extends Component { }); } - _onMetricsChange = (metrics) => { + _onMetricsChange = (metrics: AggDescriptor[]) => { this.props.onChange({ propName: 'metrics', value: metrics }); }; - _onResolutionChange = (resolution) => { + _onResolutionChange = async (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => { let newLayerType; if ( this.props.currentLayerType === LAYER_TYPE.VECTOR || @@ -76,22 +96,36 @@ export class UpdateSourceEditor extends Component { throw new Error('Unexpected layer-type'); } - this.props.onChange({ propName: 'resolution', value: resolution, newLayerType }); + await this.props.onChange( + { propName: 'metrics', value: metrics }, + { propName: 'resolution', value: resolution, newLayerType } + ); + + // Metrics editor persists metrics in state. + // Reset metricsEditorKey to force new instance and new internal state with latest metrics + this.setState({ metricsEditorKey: uuid() }); }; - _onRequestTypeSelect = (requestType) => { + _onRequestTypeSelect = (requestType: RENDER_AS) => { this.props.onChange({ propName: 'requestType', value: requestType }); }; + _getMetricsFilter() { + if (this.props.currentLayerType === LAYER_TYPE.HEATMAP) { + return (metric: EuiComboBoxOptionOption) => { + // these are countable metrics, where blending heatmap color blobs make sense + return metric.value ? isMetricCountable(metric.value) : false; + }; + } + + if (this.props.resolution === GRID_RESOLUTION.SUPER_FINE) { + return (metric: EuiComboBoxOptionOption) => { + return metric.value !== AGG_TYPE.TERMS; + }; + } + } + _renderMetricsPanel() { - const metricsFilter = - this.props.currentLayerType === LAYER_TYPE.HEATMAP - ? (metric) => { - //these are countable metrics, where blending heatmap color blobs make sense - return isMetricCountable(metric.value); - } - : null; - const allowMultipleMetrics = this.props.currentLayerType !== LAYER_TYPE.HEATMAP; return ( @@ -101,8 +135,9 @@ export class UpdateSourceEditor extends Component { {}} /> ); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts index 6359abd06d3be..aad377ef53649 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -31,7 +31,7 @@ describe('ESSearchSource', () => { const esSearchSource = new ESSearchSource(mockDescriptor); expect(esSearchSource.getMinZoom()).toBe(0); expect(esSearchSource.getMaxZoom()).toBe(24); - expect(esSearchSource.getLayerName()).toBe('source_layer'); + expect(esSearchSource.getLayerName()).toBe('hits'); }); describe('getUrlTemplateWithMeta', () => { @@ -117,21 +117,7 @@ describe('ESSearchSource', () => { }); const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters); expect(urlTemplateWithMeta.urlTemplate).toBe( - `rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape` - ); - }); - - it('should include searchSourceId in urlTemplateWithMeta', async () => { - const esSearchSource = new ESSearchSource({ - geoField: geoFieldName, - indexPatternId: 'ipId', - }); - const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta({ - ...searchFilters, - searchSessionId: '1', - }); - expect(urlTemplateWithMeta.urlTemplate).toBe( - `rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape&searchSessionId=1` + `rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))` ); }); }); @@ -162,7 +148,7 @@ describe('ESSearchSource', () => { scalingType: SCALING_TYPES.MVT, }); expect(esSearchSource.getJoinsDisabledReason()).toBe( - 'Joins are not supported when scaling by mvt vector tiles' + 'Joins are not supported when scaling by vector tiles' ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 75ec128f5a8aa..31cc07d03549a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -9,8 +9,8 @@ import _ from 'lodash'; import React, { ReactElement } from 'react'; import rison from 'rison-node'; import { i18n } from '@kbn/i18n'; -import type { Filter, IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; +import type { Filter, IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { AbstractESSource } from '../es_source'; import { @@ -35,7 +35,6 @@ import { FIELD_ORIGIN, GIS_API_PATH, MVT_GETTILE_API_PATH, - MVT_SOURCE_LAYER_NAME, MVT_TOKEN_PARAM_NAME, SCALING_TYPES, SOURCE_TYPES, @@ -54,14 +53,17 @@ import { VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; -import { TimeRange } from '../../../../../../../src/plugins/data/common'; +import { + SortDirection, + SortDirectionNumeric, + TimeRange, +} from '../../../../../../../src/plugins/data/common'; import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source'; import { ITiledSingleLayerVectorSource } from '../tiled_single_layer_vector_source'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { DataRequest } from '../../util/data_request'; -import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common'; import { isValidStringConfig } from '../../util/valid_string_config'; import { TopHitsUpdateSourceEditor } from './top_hits'; import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_source_fields'; @@ -83,6 +85,8 @@ type ESSearchSourceSyncMeta = Pick< | 'topHitsSize' >; +const ES_MVT_HITS_LAYER_NAME = 'hits'; + export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined { const timeRangeBounds = getTimeFilter().calculateBounds(timerange); return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined @@ -185,6 +189,8 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye sortOrder={this._descriptor.sortOrder} scalingType={this._descriptor.scalingType} filterByMapBounds={this.isFilterByMapBounds()} + hasJoins={sourceEditorArgs.hasJoins} + clearJoins={sourceEditorArgs.clearJoins} /> ); } @@ -211,6 +217,10 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return [this._descriptor.geoField]; } + isMvt() { + return this._descriptor.scalingType === SCALING_TYPES.MVT; + } + async getImmutableProperties(): Promise { let indexPatternName = this.getIndexPatternId(); let geoFieldType = ''; @@ -748,7 +758,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }); } else if (this._descriptor.scalingType === SCALING_TYPES.MVT) { reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReasonMvt', { - defaultMessage: 'Joins are not supported when scaling by mvt vector tiles', + defaultMessage: 'Joins are not supported when scaling by vector tiles', }); } else { reason = null; @@ -757,7 +767,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye } getLayerName(): string { - return MVT_SOURCE_LAYER_NAME; + return ES_MVT_HITS_LAYER_NAME; } async _getEditableIndex(): Promise { @@ -828,22 +838,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye `/${GIS_API_PATH}/${MVT_GETTILE_API_PATH}/{z}/{x}/{y}.pbf` ); - const geoField = await this._getGeoField(); - const urlTemplate = `${mvtUrlServicePath}\ ?geometryFieldName=${this._descriptor.geoField}\ &index=${indexPattern.title}\ -&requestBody=${risonDsl}\ -&geoFieldType=${geoField.type}`; +&requestBody=${risonDsl}`; return { refreshTokenParamName: MVT_TOKEN_PARAM_NAME, layerName: this.getLayerName(), minSourceZoom: this.getMinZoom(), maxSourceZoom: this.getMaxZoom(), - urlTemplate: searchFilters.searchSessionId - ? urlTemplate + `&searchSessionId=${searchFilters.searchSessionId}` - : urlTemplate, + urlTemplate, }; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js index c28c8bedafaf9..ad0e03c2fe09d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js @@ -35,6 +35,8 @@ export class UpdateSourceEditor extends Component { sortOrder: PropTypes.string.isRequired, scalingType: PropTypes.string.isRequired, source: PropTypes.object, + hasJoins: PropTypes.bool.isRequired, + clearJoins: PropTypes.func.isRequired, }; state = { @@ -205,6 +207,8 @@ export class UpdateSourceEditor extends Component { scalingType={this.props.scalingType} supportsClustering={this.state.supportsClustering} clusteringDisabledReason={this.state.clusteringDisabledReason} + hasJoins={this.props.hasJoins} + clearJoins={this.props.clearJoins} /> ); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap index 749d55aeb5da7..bafb3172bbc11 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap @@ -46,17 +46,7 @@ exports[`scaling form should disable clusters option when clustering is not supp /> - - - Use vector tiles for faster display of large datasets. - - } + content="Use vector tiles for faster display of large datasets." delay="regular" display="inlineBlock" position="left" @@ -127,17 +117,7 @@ exports[`scaling form should render 1`] = ` onChange={[Function]} /> - - - Use vector tiles for faster display of large datasets. - - } + content="Use vector tiles for faster display of large datasets." delay="regular" display="inlineBlock" position="left" diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.test.tsx index c02d855f13aa4..aa3c7b51df5bb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.test.tsx @@ -26,6 +26,8 @@ const defaultProps = { scalingType: SCALING_TYPES.LIMIT, supportsClustering: true, termFields: [], + hasJoins: false, + clearJoins: () => {}, }; describe('scaling form', () => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx index 2d202e934cea9..896b493507322 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx @@ -7,15 +7,14 @@ import React, { Component, Fragment } from 'react'; import { + EuiConfirmModal, EuiFormRow, - EuiHorizontalRule, EuiRadio, EuiSpacer, EuiSwitch, EuiSwitchEvent, EuiTitle, EuiToolTip, - EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -35,15 +34,20 @@ interface Props { scalingType: SCALING_TYPES; supportsClustering: boolean; clusteringDisabledReason?: string | null; + hasJoins: boolean; + clearJoins: () => void; } interface State { + nextScalingType?: SCALING_TYPES; maxResultWindow: string; + showModal: boolean; } export class ScalingForm extends Component { - state = { + state: State = { maxResultWindow: DEFAULT_MAX_RESULT_WINDOW.toLocaleString(), + showModal: false, }; _isMounted = false; @@ -68,7 +72,15 @@ export class ScalingForm extends Component { } } - _onScalingTypeChange = (optionId: string): void => { + _onScalingTypeSelect = (optionId: SCALING_TYPES): void => { + if (this.props.hasJoins && optionId !== SCALING_TYPES.LIMIT) { + this._openModal(optionId); + } else { + this._onScalingTypeChange(optionId); + } + }; + + _onScalingTypeChange = (optionId: SCALING_TYPES): void => { let layerType; if (optionId === SCALING_TYPES.CLUSTERS) { layerType = LAYER_TYPE.BLENDED_VECTOR; @@ -85,6 +97,69 @@ export class ScalingForm extends Component { this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked }); }; + _openModal = (optionId: SCALING_TYPES) => { + this.setState({ + nextScalingType: optionId, + showModal: true, + }); + }; + + _closeModal = () => { + this.setState({ + nextScalingType: undefined, + showModal: false, + }); + }; + + _acceptModal = () => { + this.props.clearJoins(); + this._onScalingTypeChange(this.state.nextScalingType!); + this._closeModal(); + }; + + _renderModal() { + if (!this.state.showModal || this.state.nextScalingType === undefined) { + return null; + } + + const scalingOptionLabel = + this.state.nextScalingType === SCALING_TYPES.CLUSTERS + ? i18n.translate('xpack.maps.source.esSearch.scalingModal.clusters', { + defaultMessage: `clusters`, + }) + : i18n.translate('xpack.maps.source.esSearch.scalingModal.vectorTiles', { + defaultMessage: `vector tiles`, + }); + return ( + +

+ +

+
+ ); + } + _renderClusteringRadio() { const clusteringRadio = ( { values: { maxResultWindow: this.state.maxResultWindow }, })} checked={this.props.scalingType === SCALING_TYPES.CLUSTERS} - onChange={() => this._onScalingTypeChange(SCALING_TYPES.CLUSTERS)} + onChange={() => this._onScalingTypeSelect(SCALING_TYPES.CLUSTERS)} disabled={!this.props.supportsClustering} /> ); @@ -108,36 +183,6 @@ export class ScalingForm extends Component { ); } - _renderMVTRadio() { - const labelText = i18n.translate('xpack.maps.source.esSearch.useMVTVectorTiles', { - defaultMessage: 'Use vector tiles', - }); - const mvtRadio = ( - this._onScalingTypeChange(SCALING_TYPES.MVT)} - /> - ); - - const enabledInfo = ( - <> - - - {i18n.translate('xpack.maps.source.esSearch.mvtDescription', { - defaultMessage: 'Use vector tiles for faster display of large datasets.', - })} - - ); - - return ( - - {mvtRadio} - - ); - } - render() { let filterByBoundsSwitch; if (this.props.scalingType === SCALING_TYPES.LIMIT) { @@ -157,6 +202,7 @@ export class ScalingForm extends Component { return ( + {this._renderModal()}
@@ -174,10 +220,24 @@ export class ScalingForm extends Component { values: { maxResultWindow: this.state.maxResultWindow }, })} checked={this.props.scalingType === SCALING_TYPES.LIMIT} - onChange={() => this._onScalingTypeChange(SCALING_TYPES.LIMIT)} + onChange={() => this._onScalingTypeSelect(SCALING_TYPES.LIMIT)} /> {this._renderClusteringRadio()} - {this._renderMVTRadio()} + + this._onScalingTypeSelect(SCALING_TYPES.MVT)} + /> + diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 0fa6335c1e0d9..ed95fdbb022fd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -20,7 +20,7 @@ import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters'; import { createExtentFilter } from '../../../../common/elasticsearch_util'; import { copyPersistentState } from '../../../reducers/copy_persistent_state'; import { DataRequestAbortError } from '../../util/data_request'; -import { expandToTileBoundaries } from '../../../../common/geo_tile_utils'; +import { expandToTileBoundaries } from '../../util/geo_tile_utils'; import { IVectorSource } from '../vector_source'; import { TimeRange } from '../../../../../../../src/plugins/data/common'; import { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap index f6d0129e85abf..b8f7af8b3844e 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap @@ -5,7 +5,6 @@ exports[`should render error for dupes 1`] = ` { _renderFieldConfig() { return this.state.currentFields.map((mvtFieldConfig: MVTFieldDescriptor, index: number) => { return ( - <> - + + {this._renderFieldNameInput(mvtFieldConfig, index)} {this._renderFieldTypeDropDown(mvtFieldConfig, index)} {this._renderFieldButtonDelete(index)} - + ); }); } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index 34a30ae9ec977..387bb9c3ca1ff 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -82,6 +82,10 @@ export class MVTSingleLayerVectorSource .filter((f) => f !== null) as MVTField[]; } + isMvt() { + return true; + } + async supportsFitToBounds() { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 4c2cffcf8b070..0ecca16fde07b 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -29,8 +29,10 @@ export type OnSourceChangeArgs = { }; export type SourceEditorArgs = { - onChange: (...args: OnSourceChangeArgs[]) => void; - currentLayerType?: string; + clearJoins: () => void; + currentLayerType: string; + hasJoins: boolean; + onChange: (...args: OnSourceChangeArgs[]) => Promise; }; export type ImmutableSourceProperty = { @@ -43,6 +45,7 @@ export interface ISource { destroy(): void; getDisplayName(): Promise; getInspectorAdapters(): Adapters | undefined; + getType(): string; isFieldAware(): boolean; isFilterByMapBounds(): boolean; isGeoGridPrecisionAware(): boolean; @@ -101,6 +104,10 @@ export class AbstractSource implements ISource { return this._inspectorAdapters; } + getType(): string { + return this._descriptor.type; + } + async getDisplayName(): Promise { return ''; } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 3c0adf64216e6..7042374296bbe 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -9,7 +9,7 @@ import type { Query } from 'src/plugins/data/common'; import { FeatureCollection, GeoJsonProperties, Geometry, Position } from 'geojson'; import { Filter, TimeRange } from 'src/plugins/data/public'; import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; -import { TooltipProperty, ITooltipProperty } from '../../tooltips/tooltip_property'; +import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; import { AbstractSource, ISource } from '../source'; import { IField } from '../../fields/field'; import { @@ -44,6 +44,7 @@ export interface BoundsRequestMeta { } export interface IVectorSource extends ISource { + isMvt(): boolean; getTooltipProperties(properties: GeoJsonProperties): Promise; getBoundsForFilters( layerDataFilters: BoundsRequestMeta, @@ -89,6 +90,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return []; } + isMvt() { + return false; + } + createField({ fieldName }: { fieldName: string }): IField { throw new Error('Not implemented'); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap index 64da5777988d1..be8c9b0750b94 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap @@ -384,546 +384,6 @@ exports[`should render 1`] = ` `; -exports[`should render line-style with label properties when ES-source is rendered as mvt 1`] = ` - - - - - - - - - - - - - - - - - - - -`; - -exports[`should render polygon-style without label properties when 3rd party mvt 1`] = ` - - - - - - - - - - - -`; - exports[`should render with no style fields 1`] = ` { class MockField extends AbstractField {} -function createLayerMock( - numFields: number, - supportedShapeTypes: VECTOR_SHAPE_TYPE[], - layerType: LAYER_TYPE = LAYER_TYPE.VECTOR, - isESSource: boolean = false -) { +function createLayerMock(numFields: number, supportedShapeTypes: VECTOR_SHAPE_TYPE[]) { const fields: IField[] = []; for (let i = 0; i < numFields; i++) { fields.push(new MockField({ fieldName: `field${i}`, origin: FIELD_ORIGIN.SOURCE })); @@ -45,17 +39,11 @@ function createLayerMock( getStyleEditorFields: async () => { return fields; }, - getType() { - return layerType; - }, getSource: () => { return { getSupportedShapeTypes: async () => { return supportedShapeTypes; }, - isESSource() { - return isESSource; - }, } as unknown as IVectorSource; }, } as unknown as IVectorLayer; @@ -111,35 +99,3 @@ test('should render with no style fields', async () => { expect(component).toMatchSnapshot(); }); - -test('should render polygon-style without label properties when 3rd party mvt', async () => { - const component = shallow( - - ); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component).toMatchSnapshot(); -}); - -test('should render line-style with label properties when ES-source is rendered as mvt', async () => { - const component = shallow( - - ); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component).toMatchSnapshot(); -}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx index d909f31315e7d..e11a560c8755f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx @@ -25,7 +25,6 @@ import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_palettes'; import { LABEL_BORDER_SIZES, - LAYER_TYPE, STYLE_TYPE, VECTOR_SHAPE_TYPE, VECTOR_STYLES, @@ -258,18 +257,7 @@ export class VectorStyleEditor extends Component { ); } - _renderLabelProperties(isPoints: boolean) { - if ( - !isPoints && - this.props.layer.getType() === LAYER_TYPE.TILED_VECTOR && - !this.props.layer.getSource().isESSource() - ) { - // This handles and edge-case - // 3rd party lines and polygons from mvt sources cannot be labeled, because they do not have label-centroid geometries inside the tile. - // These label-centroids are only added for ES-sources - return; - } - + _renderLabelProperties() { const hasLabel = this._hasLabel(); const hasLabelBorder = this._hasLabelBorder(); return ( @@ -468,7 +456,7 @@ export class VectorStyleEditor extends Component { /> - {this._renderLabelProperties(true)} + {this._renderLabelProperties()} ); } @@ -482,7 +470,7 @@ export class VectorStyleEditor extends Component { {this._renderLineWidth()} - {this._renderLabelProperties(false)} + {this._renderLabelProperties()} ); } @@ -499,7 +487,7 @@ export class VectorStyleEditor extends Component { {this._renderLineWidth()} - {this._renderLabelProperties(false)} + {this._renderLabelProperties()} ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx index 73f8736750656..bff053fc469a0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx @@ -135,7 +135,7 @@ export class DynamicColorProperty extends DynamicStyleProperty extends IStyleProperty { getFieldMetaOptions(): FieldMetaOptions; getField(): IField | null; getFieldName(): string; + getMbFieldName(): string; getFieldOrigin(): FIELD_ORIGIN | null; getRangeFieldMeta(): RangeFieldMeta | null; getCategoryFieldMeta(): CategoryFieldMeta | null; @@ -63,7 +58,7 @@ export interface IDynamicStyleProperty extends IStyleProperty { getFieldMetaRequest(): Promise; pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null; pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null; - pluckOrdinalStyleMetaFromTileMetaFeatures(features: TileMetaFeature[]): RangeFieldMeta | null; + pluckOrdinalStyleMetaFromTileMetaFeatures(metaFeatures: TileMetaFeature[]): RangeFieldMeta | null; pluckCategoricalStyleMetaFromTileMetaFeatures( features: TileMetaFeature[] ): CategoryFieldMeta | null; @@ -213,6 +208,10 @@ export class DynamicStyleProperty return this._field ? this._field.getName() : ''; } + getMbFieldName() { + return this._field ? this._field.getMbFieldName() : ''; + } + isDynamic() { return true; } @@ -314,54 +313,36 @@ export class DynamicStyleProperty return null; } - const name = this.getFieldName(); + const mbFieldName = this.getMbFieldName(); let min = Infinity; let max = -Infinity; for (let i = 0; i < metaFeatures.length; i++) { - const fieldMeta = metaFeatures[i].properties.fieldMeta; - if (fieldMeta && fieldMeta[name] && fieldMeta[name].range) { - min = Math.min(fieldMeta[name].range?.min as number, min); - max = Math.max(fieldMeta[name].range?.max as number, max); + const fieldMeta = metaFeatures[i].properties; + const minField = `aggregations.${mbFieldName}.min`; + const maxField = `aggregations.${mbFieldName}.max`; + if ( + fieldMeta && + typeof fieldMeta[minField] === 'number' && + typeof fieldMeta[maxField] === 'number' + ) { + min = Math.min(fieldMeta[minField] as number, min); + max = Math.max(fieldMeta[maxField] as number, max); } } - return { - min, - max, - delta: max - min, - }; + + return min === Infinity || max === -Infinity + ? null + : { + min, + max, + delta: max - min, + }; } pluckCategoricalStyleMetaFromTileMetaFeatures( metaFeatures: TileMetaFeature[] ): CategoryFieldMeta | null { - const size = this.getNumberOfCategories(); - if (!this.isCategorical() || size <= 0) { - return null; - } - - const name = this.getFieldName(); - - const counts = new Map(); - for (let i = 0; i < metaFeatures.length; i++) { - const fieldMeta = metaFeatures[i].properties.fieldMeta; - if (fieldMeta && fieldMeta[name] && fieldMeta[name].categories) { - const categoryFieldMeta: CategoryFieldMeta = fieldMeta[name] - .categories as CategoryFieldMeta; - for (let c = 0; c < categoryFieldMeta.categories.length; c++) { - const category: Category = categoryFieldMeta.categories[c]; - // properties object may be sparse, so need to check if the field is effectively present - if (typeof category.key !== undefined) { - if (counts.has(category.key)) { - counts.set(category.key, (counts.get(category.key) as number) + category.count); - } else { - counts.set(category.key, category.count); - } - } - } - } - } - - return trimCategories(counts, size); + return null; } pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null { @@ -370,9 +351,24 @@ export class DynamicStyleProperty } const name = this.getFieldName(); - return pluckRangeFieldMeta(features, name, (rawValue: unknown) => { - return parseFloat(rawValue as string); - }); + let min = Infinity; + let max = -Infinity; + for (let i = 0; i < features.length; i++) { + const feature = features[i]; + const newValue = feature.properties ? parseFloat(feature.properties[name]) : NaN; + if (!isNaN(newValue)) { + min = Math.min(min, newValue); + max = Math.max(max, newValue); + } + } + + return min === Infinity || max === -Infinity + ? null + : { + min, + max, + delta: max - min, + }; } pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null { @@ -381,7 +377,32 @@ export class DynamicStyleProperty return null; } - return pluckCategoryFieldMeta(features, this.getFieldName(), size); + const counts = new Map(); + for (let i = 0; i < features.length; i++) { + const feature = features[i]; + const term = feature.properties ? feature.properties[this.getFieldName()] : undefined; + // properties object may be sparse, so need to check if the field is effectively present + if (typeof term !== undefined) { + if (counts.has(term)) { + counts.set(term, counts.get(term) + 1); + } else { + counts.set(term, 1); + } + } + } + + const ordered = []; + for (const [key, value] of counts) { + ordered.push({ key, count: value }); + } + + ordered.sort((a, b) => { + return b.count - a.count; + }); + const truncated = ordered.slice(0, size); + return { + categories: truncated, + } as CategoryFieldMeta; } _pluckOrdinalStyleMetaFromFieldMetaData(styleMetaData: StyleMetaData): RangeFieldMeta | null { @@ -487,7 +508,7 @@ export class DynamicStyleProperty targetName = getComputedFieldName(this.getStyleName(), this._field.getName()); } else { // Non-geojson sources (e.g. 3rd party mvt or ES-source as mvt) - targetName = this._field.getName(); + targetName = this._field.getMbFieldName(); } } return targetName; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 1e7267b9e1e32..058ee0db08d35 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -16,7 +16,6 @@ import { FIELD_ORIGIN, GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE, - KBN_VECTOR_SHAPE_TYPE_COUNTS, LAYER_STYLE_TYPE, SOURCE_FORMATTERS_DATA_REQUEST_ID, STYLE_TYPE, @@ -76,7 +75,6 @@ import { IVectorLayer } from '../../layers/vector_layer'; import { IVectorSource } from '../../sources/vector_source'; import { createStyleFieldsHelper, StyleFieldsHelper } from './style_fields_helper'; import { IESAggField } from '../../fields/agg'; -import { VectorShapeTypeCounts } from '../../../../common/get_geometry_counts'; const POINTS = [GEO_JSON_TYPE.POINT, GEO_JSON_TYPE.MULTI_POINT]; const LINES = [GEO_JSON_TYPE.LINE_STRING, GEO_JSON_TYPE.MULTI_LINE_STRING]; @@ -92,9 +90,8 @@ export interface IVectorStyle extends IStyle { previousFields: IField[], mapColors: string[] ): Promise<{ hasChanges: boolean; nextStyleDescriptor?: VectorStyleDescriptor }>; - isTimeAware: () => boolean; - getIcon: () => ReactElement; - getIconFromGeometryTypes: (isLinesOnly: boolean, isPointsOnly: boolean) => ReactElement; + isTimeAware(): boolean; + getIcon(): ReactElement; hasLegendDetails: () => Promise; renderLegendDetails: () => ReactElement; clearFeatureState: (featureCollection: FeatureCollection, mbMap: MbMap, sourceId: string) => void; @@ -492,50 +489,16 @@ export class VectorStyle implements IVectorStyle { } async pluckStyleMetaFromTileMeta(metaFeatures: TileMetaFeature[]): Promise { - const shapeTypeCountMeta: VectorShapeTypeCounts = metaFeatures.reduce( - (accumulator: VectorShapeTypeCounts, tileMeta: TileMetaFeature) => { - if ( - !tileMeta || - !tileMeta.properties || - !tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS] - ) { - return accumulator; - } - - accumulator[VECTOR_SHAPE_TYPE.POINT] += - tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS][VECTOR_SHAPE_TYPE.POINT]; - accumulator[VECTOR_SHAPE_TYPE.LINE] += - tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS][VECTOR_SHAPE_TYPE.LINE]; - accumulator[VECTOR_SHAPE_TYPE.POLYGON] += - tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS][VECTOR_SHAPE_TYPE.POLYGON]; - - return accumulator; - }, - { - [VECTOR_SHAPE_TYPE.POLYGON]: 0, - [VECTOR_SHAPE_TYPE.LINE]: 0, - [VECTOR_SHAPE_TYPE.POINT]: 0, - } - ); - - const isLinesOnly = - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.LINE] > 0 && - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POINT] === 0 && - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POLYGON] === 0; - const isPointsOnly = - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.LINE] === 0 && - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POINT] > 0 && - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POLYGON] === 0; - const isPolygonsOnly = - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.LINE] === 0 && - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POINT] === 0 && - shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POLYGON] > 0; - + const supportedShapeTypes = await this._source.getSupportedShapeTypes(); const styleMeta: StyleMetaDescriptor = { geometryTypes: { - isPointsOnly, - isLinesOnly, - isPolygonsOnly, + isPointsOnly: + supportedShapeTypes.length === 1 && supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.POINT), + isLinesOnly: + supportedShapeTypes.length === 1 && supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.LINE), + isPolygonsOnly: + supportedShapeTypes.length === 1 && + supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.POLYGON), }, fieldMeta: {}, }; @@ -737,7 +700,7 @@ export class VectorStyle implements IVectorStyle { : (this._iconStyleProperty as StaticIconProperty).getOptions().value; } - getIconFromGeometryTypes(isLinesOnly: boolean, isPointsOnly: boolean) { + _getIconFromGeometryTypes(isLinesOnly: boolean, isPointsOnly: boolean) { let strokeColor; if (isLinesOnly) { strokeColor = extractColorFromStyleProperty( @@ -771,7 +734,7 @@ export class VectorStyle implements IVectorStyle { getIcon() { const isLinesOnly = this._getIsLinesOnly(); const isPointsOnly = this._getIsPointsOnly(); - return this.getIconFromGeometryTypes(isLinesOnly, isPointsOnly); + return this._getIconFromGeometryTypes(isLinesOnly, isPointsOnly); } _getLegendDetailStyleProperties = () => { diff --git a/x-pack/plugins/maps/common/geo_tile_utils.test.js b/x-pack/plugins/maps/public/classes/util/geo_tile_utils.test.ts similarity index 100% rename from x-pack/plugins/maps/common/geo_tile_utils.test.js rename to x-pack/plugins/maps/public/classes/util/geo_tile_utils.test.ts diff --git a/x-pack/plugins/maps/common/geo_tile_utils.ts b/x-pack/plugins/maps/public/classes/util/geo_tile_utils.ts similarity index 95% rename from x-pack/plugins/maps/common/geo_tile_utils.ts rename to x-pack/plugins/maps/public/classes/util/geo_tile_utils.ts index 1a8ac3cbe17ae..6e82d3b509565 100644 --- a/x-pack/plugins/maps/common/geo_tile_utils.ts +++ b/x-pack/plugins/maps/public/classes/util/geo_tile_utils.ts @@ -6,9 +6,9 @@ */ import _ from 'lodash'; -import { DECIMAL_DEGREES_PRECISION } from './constants'; -import { clampToLatBounds } from './elasticsearch_util'; -import { MapExtent } from './descriptor_types'; +import { DECIMAL_DEGREES_PRECISION } from '../../../common/constants'; +import { clampToLatBounds } from '../../../common/elasticsearch_util'; +import { MapExtent } from '../../../common/descriptor_types'; const ZOOM_TILE_KEY_INDEX = 0; const X_TILE_KEY_INDEX = 1; diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index 68efd416718fd..544b2697cab43 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -9,7 +9,6 @@ import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME, KBN_IS_CENTROID_FEATURE, - KBN_METADATA_FEATURE, } from '../../../common/constants'; import { Timeslice } from '../../../common/descriptor_types'; @@ -19,7 +18,6 @@ export interface TimesliceMaskConfig { timeslice: Timeslice; } -export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_METADATA_FEATURE], true]; export const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true]; function getFilterExpression( @@ -56,7 +54,6 @@ export function getFillFilterExpression( ): unknown[] { return getFilterExpression( [ - EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES, [ 'any', @@ -75,7 +72,6 @@ export function getLineFilterExpression( ): unknown[] { return getFilterExpression( [ - EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES, [ 'any', @@ -96,7 +92,6 @@ export function getPointFilterExpression( ): unknown[] { return getFilterExpression( [ - EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES, [ 'any', @@ -109,13 +104,17 @@ export function getPointFilterExpression( ); } -export function getCentroidFilterExpression( +export function getLabelFilterExpression( hasJoins: boolean, + isSourceGeoJson: boolean, timesliceMaskConfig?: TimesliceMaskConfig ): unknown[] { - return getFilterExpression( - [EXCLUDE_TOO_MANY_FEATURES_BOX, ['==', ['get', KBN_IS_CENTROID_FEATURE], true]], - hasJoins, - timesliceMaskConfig - ); + const filters: unknown[] = []; + + // centroids added for geojson sources only + if (isSourceGeoJson) { + filters.push(['==', ['get', KBN_IS_CENTROID_FEATURE], true]); + } + + return getFilterExpression(filters, hasJoins, timesliceMaskConfig); } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap index 5fb1cc6f72585..1a3e97ee4fae1 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap @@ -98,7 +98,9 @@ exports[`EditLayerPanel is rendered 1`] = ` "getId": [Function], "getImmutableSourceProperties": [Function], "getLayerTypeIconName": [Function], + "getType": [Function], "hasErrors": [Function], + "hasJoins": [Function], "renderSourceSettingsEditor": [Function], "showJoinEditor": [Function], "supportsElasticsearchFilters": [Function], @@ -119,7 +121,9 @@ exports[`EditLayerPanel is rendered 1`] = ` "getId": [Function], "getImmutableSourceProperties": [Function], "getLayerTypeIconName": [Function], + "getType": [Function], "hasErrors": [Function], + "hasJoins": [Function], "renderSourceSettingsEditor": [Function], "showJoinEditor": [Function], "supportsElasticsearchFilters": [Function], diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx index e66dc8a10948f..82795b8bd9317 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx @@ -48,6 +48,7 @@ jest.mock('../../kibana_services', () => { import React from 'react'; import { shallow } from 'enzyme'; +import { LAYER_TYPE } from '../../../common/constants'; import { ILayer } from '../../classes/layers/layer'; import { EditLayerPanel } from './edit_layer_panel'; @@ -55,6 +56,9 @@ const mockLayer = { getId: () => { return '1'; }, + getType: () => { + return LAYER_TYPE.VECTOR; + }, getDisplayName: () => { return 'layer 1'; }, @@ -79,6 +83,9 @@ const mockLayer = { hasErrors: () => { return false; }, + hasJoins: () => { + return false; + }, supportsFitToBounds: () => { return true; }, @@ -87,7 +94,8 @@ const mockLayer = { const defaultProps = { selectedLayer: mockLayer, fitToBounds: () => {}, - updateSourceProp: () => {}, + updateSourceProps: async () => {}, + clearJoins: () => {}, }; describe('EditLayerPanel', () => { diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx index 424c4b8e16bec..7ba2f74ddc7d9 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx @@ -30,7 +30,6 @@ import { StyleSettings } from './style_settings'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { LAYER_TYPE } from '../../../common/constants'; import { getData, getCore } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer'; @@ -40,13 +39,9 @@ import { IField } from '../../classes/fields/field'; const localStorage = new Storage(window.localStorage); export interface Props { + clearJoins: (layer: ILayer) => void; selectedLayer?: ILayer; - updateSourceProp: ( - layerId: string, - propName: string, - value: unknown, - newLayerType?: LAYER_TYPE - ) => void; + updateSourceProps: (layerId: string, sourcePropChanges: OnSourceChangeArgs[]) => Promise; } interface State { @@ -141,9 +136,12 @@ export class EditLayerPanel extends Component { } _onSourceChange = (...args: OnSourceChangeArgs[]) => { - for (let i = 0; i < args.length; i++) { - const { propName, value, newLayerType } = args[i]; - this.props.updateSourceProp(this.props.selectedLayer!.getId(), propName, value, newLayerType); + return this.props.updateSourceProps(this.props.selectedLayer!.getId(), args); + }; + + _clearJoins = () => { + if (this.props.selectedLayer) { + this.props.clearJoins(this.props.selectedLayer); } }; @@ -279,6 +277,11 @@ export class EditLayerPanel extends Component { /> {this.props.selectedLayer.renderSourceSettingsEditor({ + clearJoins: this._clearJoins, + currentLayerType: this.props.selectedLayer.getType(), + hasJoins: isVectorLayer(this.props.selectedLayer) + ? (this.props.selectedLayer as IVectorLayer).hasJoins() + : false, onChange: this._onSourceChange, })} diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts index 84caa45741a62..5f9a920d38494 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts @@ -9,11 +9,12 @@ import { AnyAction } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { EditLayerPanel } from './edit_layer_panel'; -import { LAYER_TYPE } from '../../../common/constants'; import { getSelectedLayer } from '../../selectors/map_selectors'; -import { updateSourceProp } from '../../actions'; +import { setJoinsForLayer, updateSourceProps } from '../../actions'; import { MapStoreState } from '../../reducers/store'; +import { ILayer } from '../../classes/layers/layer'; import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer'; +import { OnSourceChangeArgs } from '../../classes/sources/source'; function mapStateToProps(state: MapStoreState) { const selectedLayer = getSelectedLayer(state); @@ -31,8 +32,11 @@ function mapStateToProps(state: MapStoreState) { function mapDispatchToProps(dispatch: ThunkDispatch) { return { - updateSourceProp: (id: string, propName: string, value: unknown, newLayerType?: LAYER_TYPE) => - dispatch(updateSourceProp(id, propName, value, newLayerType)), + clearJoins: (layer: ILayer) => { + dispatch(setJoinsForLayer(layer, [])); + }, + updateSourceProps: async (id: string, sourcePropChanges: OnSourceChangeArgs[]) => + await dispatch(updateSourceProps(id, sourcePropChanges)), }; } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx index e99ec6a688092..24cdd9b7dc813 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx @@ -90,9 +90,7 @@ export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDispla ) : ( {renderJoins()} - - { ] as [MbPoint, MbPoint]; const selectedFeatures = this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbEditLayerIds, - filter: ['all', EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES], + filter: ['all', EXCLUDE_CENTROID_FEATURES], }); if (!selectedFeatures.length) { return; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 053e410b8c712..93dfebecd1c34 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -33,7 +33,6 @@ import { } from '../../../common/descriptor_types'; import { DECIMAL_DEGREES_PRECISION, - KBN_TOO_MANY_FEATURES_IMAGE_ID, LAYER_TYPE, RawValue, ZOOM_PRECISION, @@ -209,14 +208,6 @@ export class MbMap extends Component { }, }); - const tooManyFeaturesImageSrc = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARLSURBVHic7ZnPbxRVAMe/7735sWO3293ZlUItJsivCxEE0oTYRgu1FqTQoFSwKTYx8SAH/wHjj4vRozGGi56sMcW2UfqTEuOhppE0KJc2GIuKQFDY7qzdtrudX88D3YTUdFuQN8+k87ltZt7uZz958/bNLAGwBWsYKltANmEA2QKyCQPIFpBNGEC2gGzCALIFZBMGkC0gmzCAbAHZhAFkC8gmDCBbQDZhANkCslnzARQZH6oDpNs0D5UDSUIInePcOpPLfdfnODNBuwQWIAWwNOABwHZN0x8npE6hNLJ4DPWRyFSf40wE5VOEQPBjcR0g3YlE4ybGmtK+/1NzJtOZA/xSYwZMs3nG962T2ez3It2AANaA/kSidYuivOQBs5WM1fUnk6f0u+GXJUqIuUtVXx00zRbRfkIDfBqL7a1WlIYbjvNtTTr99jXXHVpH6dMjK0R4cXq6c9rzxjcx9sKX8XitSEdhAToMI7VP10/97fsTh7PZrgWAN1lW72KE2vOm2b5chDTgtWQyn93x/bEEIetEOQIC14CxVOr1CkKefH929t0v8vn0vcdGEoljGxXl4C3PGz2YyXy+AHARDqtByAxoUdWKBKV70r4/vvTLA0CjZfX+5nkDGxirKzUTgkBIgNaysh3gnF627R+XO+dQJvP1ddcdrmSsbtA020pF+CAW21qrqmUiXIUEqGRsIwD0FQq/lzqv0bJ6rrvucBVjzwyb5ivLRTiiaW+8VV7eIEBVTAANiIIQd9RxZlc6t9Gyem647vn1jD07ZJonl4sQASoevqmgABzwwHnJzc69PGdZ3X+47sgGxuqHTPPE0ggeVtg5/QeEBMhxPg1Aa1DV2GrHPG9ZXy1G2D+wNALn9jyQEeHKAJgP+033Kgrdqij7AFwZtu3bqx3XWShMHtV1o1pRGo4YxiNd+fyEB2DKdX/4aG5u0hbwcylkBryTy/3scT6zW9Nq7ndso2Wdvea6Q1WUHuiPx1/WAXLBcWZXun94UMRcAoD/p+ddTFK6u8MwUvc7vsmyem+67oVqVT0wkEgcF+FYRNhW+L25uX6f84XThtHxIBudE5bVY/t++jFVrU/dvVSFICzAqG3PX/S8rihj2/61qK1AOUB7ksl2jdLUL7Z9rvgcQQRCFsEi5wqFmw26XnhCUQ63GcZmCly95Lrzpca0G0byk3j8tEnpU1c975tmyxoU5QcE8EAEAM5WVOzfoarHAeC2749dcpzxMwsLv07Ztg0AOzVNf03Ttu/S9T2PMlbjc25fdpyutmx2TLRbIAEA4M1otKo1EjmaoHQn4ZwBgA/kAVAK6MXXdzxv/ONcrq/HcbJBeAUWoEizqsaORaPbKglZrxMSZZyrM76f/ovzWx/m85PFWREUgQf4v7Hm/xcIA8gWkE0YQLaAbMIAsgVkEwaQLSCbMIBsAdmEAWQLyCYMIFtANmEA2QKyCQPIFpDNmg/wD3OFdEybUvJjAAAAAElFTkSuQmCC'; - const tooManyFeaturesImage = new Image(); - tooManyFeaturesImage.onload = () => { - mbMap.addImage(KBN_TOO_MANY_FEATURES_IMAGE_ID, tooManyFeaturesImage); - }; - tooManyFeaturesImage.src = tooManyFeaturesImageSrc; - let emptyImage: HTMLImageElement; mbMap.on('styleimagemissing', (e: unknown) => { if (emptyImage) { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx index 7e79113d6b242..04b1d2205644f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx @@ -36,6 +36,19 @@ const mockLayer = { canShowTooltip: () => { return true; }, + getMbTooltipLayerIds: () => { + return ['foo', 'bar']; + }, + getSource: () => { + return { + isMvt: () => { + return false; + }, + isESSource: () => { + return false; + }, + }; + }, getFeatureById: () => { return { geometry: { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index c2ad75d9cb335..c2b89e64a449b 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -19,12 +19,7 @@ import uuid from 'uuid/v4'; import { Geometry } from 'geojson'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; -import { - FEATURE_ID_PROPERTY_NAME, - GEO_JSON_TYPE, - LON_INDEX, - RawValue, -} from '../../../../common/constants'; +import { GEO_JSON_TYPE, LON_INDEX, RawValue } from '../../../../common/constants'; import { GEOMETRY_FILTER_ACTION, TooltipFeature, @@ -33,9 +28,8 @@ import { } from '../../../../common/descriptor_types'; import { TooltipPopover } from './tooltip_popover'; import { FeatureGeometryFilterForm } from './features_tooltip'; -import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions'; import { ILayer } from '../../../classes/layers/layer'; -import { IVectorLayer, isVectorLayer } from '../../../classes/layers/vector_layer'; +import { IVectorLayer, isVectorLayer, getFeatureId } from '../../../classes/layers/vector_layer'; import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property'; function justifyAnchorLocation( @@ -132,7 +126,13 @@ export class TooltipControl extends Component { }) as IVectorLayer; } - _loadPreIndexedShape = async ({ layerId, featureId }: { layerId: string; featureId: string }) => { + _loadPreIndexedShape = async ({ + layerId, + featureId, + }: { + layerId: string; + featureId?: string | number; + }) => { const tooltipLayer = this._findLayerById(layerId); if (!tooltipLayer || typeof featureId === 'undefined') { return null; @@ -152,7 +152,7 @@ export class TooltipControl extends Component { tooltipId, }: { layerId: string; - featureId: string; + featureId?: string | number; tooltipId: string; }): TooltipFeatureAction[] { const actions = []; @@ -203,7 +203,8 @@ export class TooltipControl extends Component { if (!layer) { break; } - const featureId = mbFeature.properties?.[FEATURE_ID_PROPERTY_NAME]; + + const featureId = getFeatureId(mbFeature, layer.getSource()); const layerId = layer.getId(); let match = false; for (let j = 0; j < uniqueFeatures.length; j++) { @@ -284,9 +285,10 @@ export class TooltipControl extends Component { } const targetMbFeature = mbFeatures[0]; - if (this.props.openTooltips[0] && this.props.openTooltips[0].features.length) { + const layer = this._getLayerByMbLayerId(targetMbFeature.layer.id); + if (layer && this.props.openTooltips[0] && this.props.openTooltips[0].features.length) { const firstFeature = this.props.openTooltips[0].features[0]; - if (targetMbFeature.properties?.[FEATURE_ID_PROPERTY_NAME] === firstFeature.id) { + if (getFeatureId(targetMbFeature, layer.getSource()) === firstFeature.id) { // ignore hover events when hover tooltip is all ready opened for feature return; } @@ -312,7 +314,7 @@ export class TooltipControl extends Component { (accumulator: string[], layer: ILayer) => { // tooltips are only supported for vector layers, filter out all other layer types return layer.isVisible() && isVectorLayer(layer) - ? accumulator.concat(layer.getMbLayerIds()) + ? accumulator.concat((layer as IVectorLayer).getMbTooltipLayerIds()) : accumulator; }, [] @@ -347,7 +349,6 @@ export class TooltipControl extends Component { ] as [MbPoint, MbPoint]; return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds, - filter: EXCLUDE_TOO_MANY_FEATURES_BOX, }); } diff --git a/x-pack/plugins/maps/server/kibana_server_services.ts b/x-pack/plugins/maps/server/kibana_server_services.ts index e3c612f415c4d..f5bd4dad085d8 100644 --- a/x-pack/plugins/maps/server/kibana_server_services.ts +++ b/x-pack/plugins/maps/server/kibana_server_services.ts @@ -20,14 +20,17 @@ export const setInternalRepository = ( }; export const getInternalRepository = () => internalRepository; +let esClient: ElasticsearchClient; let indexPatternsService: IndexPatternsCommonService; export const setIndexPatternsService = async ( indexPatternsServiceFactory: IndexPatternsServiceStart['indexPatternsServiceFactory'], elasticsearchClient: ElasticsearchClient ) => { + esClient = elasticsearchClient; indexPatternsService = await indexPatternsServiceFactory( new SavedObjectsClient(getInternalRepository()), elasticsearchClient ); }; export const getIndexPatternsService = () => indexPatternsService; +export const getESClient = () => esClient; diff --git a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts new file mode 100644 index 0000000000000..cfc894d512450 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'src/core/server'; +import type { DataRequestHandlerContext } from 'src/plugins/data/server'; +import { RENDER_AS } from '../../common/constants'; + +function isAbortError(error: Error) { + return error.message === 'Request aborted' || error.message === 'Aborted'; +} + +export async function getEsGridTile({ + logger, + context, + index, + geometryFieldName, + x, + y, + z, + requestBody = {}, + requestType = RENDER_AS.POINT, +}: { + x: number; + y: number; + z: number; + geometryFieldName: string; + index: string; + context: DataRequestHandlerContext; + logger: Logger; + requestBody: any; + requestType: RENDER_AS.GRID | RENDER_AS.POINT; +}): Promise { + try { + const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`; + const body = { + size: 0, // no hits + grid_precision: 7, + exact_bounds: false, + extent: 4096, // full resolution, + query: requestBody.query, + grid_type: requestType === RENDER_AS.GRID ? 'grid' : 'centroid', + aggs: requestBody.aggs, + fields: requestBody.fields, + runtime_mappings: requestBody.runtime_mappings, + }; + const tile = await context.core.elasticsearch.client.asCurrentUser.transport.request({ + method: 'GET', + path, + body, + }); + return tile.body as unknown as Buffer; + } catch (e) { + if (!isAbortError(e)) { + // These are often circuit breaking exceptions + // Should return a tile with some error message + logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`); + } + return null; + } +} diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 11e1af5a7d368..0864b373af3f8 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -5,53 +5,15 @@ * 2.0. */ -// @ts-expect-error -import geojsonvt from 'geojson-vt'; -// @ts-expect-error -import vtpbf from 'vt-pbf'; +import _ from 'lodash'; import { Logger } from 'src/core/server'; import type { DataRequestHandlerContext } from 'src/plugins/data/server'; -import { Feature, FeatureCollection, Polygon } from 'geojson'; -import { countVectorShapeTypes } from '../../common/get_geometry_counts'; -import { - COUNT_PROP_NAME, - ES_GEO_FIELD_TYPE, - FEATURE_ID_PROPERTY_NAME, - GEOTILE_GRID_AGG_NAME, - KBN_FEATURE_COUNT, - KBN_IS_TILE_COMPLETE, - KBN_METADATA_FEATURE, - KBN_VECTOR_SHAPE_TYPE_COUNTS, - MAX_ZOOM, - MVT_SOURCE_LAYER_NAME, - RENDER_AS, - SUPER_FINE_ZOOM_DELTA, - VECTOR_SHAPE_TYPE, -} from '../../common/constants'; - -import { - createExtentFilter, - convertRegularRespToGeoJson, - hitsToGeoJson, - isTotalHitsGreaterThan, - formatEnvelopeAsPolygon, - TotalHits, -} from '../../common/elasticsearch_util'; -import { flattenHit } from './util'; -import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; -import { getCentroidFeatures } from '../../common/get_centroid_features'; -import { pluckRangeFieldMeta } from '../../common/pluck_range_field_meta'; -import { FieldMeta, TileMetaFeature } from '../../common/descriptor_types'; -import { pluckCategoryFieldMeta } from '../../common/pluck_category_field_meta'; - -// heuristic. largest color-palette has 30 colors. 1 color is used for 'other'. -const TERM_COUNT = 30 - 1; function isAbortError(error: Error) { return error.message === 'Request aborted' || error.message === 'Aborted'; } -export async function getGridTile({ +export async function getEsTile({ logger, context, index, @@ -60,9 +22,6 @@ export async function getGridTile({ y, z, requestBody = {}, - requestType = RENDER_AS.POINT, - searchSessionId, - abortSignal, }: { x: number; y: number; @@ -72,388 +31,32 @@ export async function getGridTile({ context: DataRequestHandlerContext; logger: Logger; requestBody: any; - requestType: RENDER_AS.GRID | RENDER_AS.POINT; - geoFieldType: ES_GEO_FIELD_TYPE; - searchSessionId?: string; - abortSignal: AbortSignal; }): Promise { try { - const tileBounds: ESBounds = tileToESBbox(x, y, z); - requestBody.query.bool.filter.push(getTileSpatialFilter(geometryFieldName, tileBounds)); - requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min( - z + SUPER_FINE_ZOOM_DELTA, - MAX_ZOOM - ); - requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; - requestBody.track_total_hits = false; - - const response = await context - .search!.search( - { - params: { - index, - body: requestBody, - }, - }, - { - sessionId: searchSessionId, - legacyHitsTotal: false, - abortSignal, - } - ) - .toPromise(); - const features: Feature[] = convertRegularRespToGeoJson(response.rawResponse, requestType); - - if (features.length) { - const bounds = formatEnvelopeAsPolygon({ - maxLat: tileBounds.top_left.lat, - minLat: tileBounds.bottom_right.lat, - maxLon: tileBounds.bottom_right.lon, - minLon: tileBounds.top_left.lon, - }); - - const fieldNames = new Set(); - features.forEach((feature) => { - for (const key in feature.properties) { - if (feature.properties.hasOwnProperty(key) && key !== 'key' && key !== 'gridCentroid') { - fieldNames.add(key); - } - } - }); - - const fieldMeta: FieldMeta = {}; - fieldNames.forEach((fieldName: string) => { - const rangeMeta = pluckRangeFieldMeta(features, fieldName, (rawValue: unknown) => { - if (fieldName === COUNT_PROP_NAME) { - return parseFloat(rawValue as string); - } else if (typeof rawValue === 'number') { - return rawValue; - } else if (rawValue) { - return parseFloat((rawValue as { value: string }).value); - } else { - return NaN; - } - }); - - const categoryMeta = pluckCategoryFieldMeta(features, fieldName, TERM_COUNT); - - if (!fieldMeta[fieldName]) { - fieldMeta[fieldName] = {}; - } - - if (rangeMeta) { - fieldMeta[fieldName].range = rangeMeta; - } - - if (categoryMeta) { - fieldMeta[fieldName].categories = categoryMeta; - } - }); - - const metaDataFeature: TileMetaFeature = { - type: 'Feature', - properties: { - [KBN_METADATA_FEATURE]: true, - [KBN_FEATURE_COUNT]: features.length, - [KBN_IS_TILE_COMPLETE]: true, - [KBN_VECTOR_SHAPE_TYPE_COUNTS]: - requestType === RENDER_AS.GRID - ? { - [VECTOR_SHAPE_TYPE.POINT]: 0, - [VECTOR_SHAPE_TYPE.LINE]: 0, - [VECTOR_SHAPE_TYPE.POLYGON]: features.length, - } - : { - [VECTOR_SHAPE_TYPE.POINT]: features.length, - [VECTOR_SHAPE_TYPE.LINE]: 0, - [VECTOR_SHAPE_TYPE.POLYGON]: 0, - }, - fieldMeta, - }, - geometry: bounds, - }; - - features.push(metaDataFeature); - } - - const featureCollection: FeatureCollection = { - features, - type: 'FeatureCollection', + const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`; + let fields = _.uniq(requestBody.docvalue_fields.concat(requestBody.stored_fields)); + fields = fields.filter((f) => f !== geometryFieldName); + const body = { + grid_precision: 0, // no aggs + exact_bounds: true, + extent: 4096, // full resolution, + query: requestBody.query, + fields, + runtime_mappings: requestBody.runtime_mappings, + track_total_hits: requestBody.size + 1, }; - - return createMvtTile(featureCollection, z, x, y); + const tile = await context.core.elasticsearch.client.asCurrentUser.transport.request({ + method: 'GET', + path, + body, + }); + return tile.body as unknown as Buffer; } catch (e) { if (!isAbortError(e)) { // These are often circuit breaking exceptions // Should return a tile with some error message - logger.warn(`Cannot generate grid-tile for ${z}/${x}/${y}: ${e.message}`); + logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`); } return null; } } - -export async function getTile({ - logger, - context, - index, - geometryFieldName, - x, - y, - z, - requestBody = {}, - geoFieldType, - searchSessionId, - abortSignal, -}: { - x: number; - y: number; - z: number; - geometryFieldName: string; - index: string; - context: DataRequestHandlerContext; - logger: Logger; - requestBody: any; - geoFieldType: ES_GEO_FIELD_TYPE; - searchSessionId?: string; - abortSignal: AbortSignal; -}): Promise { - let features: Feature[]; - try { - requestBody.query.bool.filter.push( - getTileSpatialFilter(geometryFieldName, tileToESBbox(x, y, z)) - ); - - const searchOptions = { - sessionId: searchSessionId, - legacyHitsTotal: false, - abortSignal, - }; - - const countResponse = await context - .search!.search( - { - params: { - index, - body: { - size: 0, - query: requestBody.query, - track_total_hits: requestBody.size + 1, - }, - }, - }, - searchOptions - ) - .toPromise(); - - if ( - isTotalHitsGreaterThan( - countResponse.rawResponse.hits.total as unknown as TotalHits, - requestBody.size - ) - ) { - // Generate "too many features"-bounds - const bboxResponse = await context - .search!.search( - { - params: { - index, - body: { - size: 0, - query: requestBody.query, - aggs: { - data_bounds: { - geo_bounds: { - field: geometryFieldName, - }, - }, - }, - track_total_hits: false, - }, - }, - }, - searchOptions - ) - .toPromise(); - - const metaDataFeature: TileMetaFeature = { - type: 'Feature', - properties: { - [KBN_METADATA_FEATURE]: true, - [KBN_IS_TILE_COMPLETE]: false, - [KBN_FEATURE_COUNT]: 0, - [KBN_VECTOR_SHAPE_TYPE_COUNTS]: { - [VECTOR_SHAPE_TYPE.POINT]: 0, - [VECTOR_SHAPE_TYPE.LINE]: 0, - [VECTOR_SHAPE_TYPE.POLYGON]: 0, - }, - }, - geometry: esBboxToGeoJsonPolygon( - // @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response - bboxResponse.rawResponse.aggregations.data_bounds.bounds, - tileToESBbox(x, y, z) - ), - }; - features = [metaDataFeature]; - } else { - const documentsResponse = await context - .search!.search( - { - params: { - index, - body: { - ...requestBody, - track_total_hits: false, - }, - }, - }, - searchOptions - ) - .toPromise(); - - const featureCollection = hitsToGeoJson( - // @ts-expect-error hitsToGeoJson should be refactored to accept estypes.SearchHit - documentsResponse.rawResponse.hits.hits, - (hit: Record) => { - return flattenHit(geometryFieldName, hit); - }, - geometryFieldName, - geoFieldType, - [] - ); - - features = featureCollection.features; - - // Correct system-fields. - for (let i = 0; i < features.length; i++) { - const props = features[i].properties; - if (props !== null) { - props[FEATURE_ID_PROPERTY_NAME] = features[i].id; - } - } - - const counts = countVectorShapeTypes(features); - - const fieldNames = new Set(); - features.forEach((feature) => { - for (const key in feature.properties) { - if ( - feature.properties.hasOwnProperty(key) && - key !== '_index' && - key !== '_id' && - key !== FEATURE_ID_PROPERTY_NAME - ) { - fieldNames.add(key); - } - } - }); - - const fieldMeta: FieldMeta = {}; - fieldNames.forEach((fieldName: string) => { - const rangeMeta = pluckRangeFieldMeta(features, fieldName, (rawValue: unknown) => { - return typeof rawValue === 'number' ? rawValue : NaN; - }); - const categoryMeta = pluckCategoryFieldMeta(features, fieldName, TERM_COUNT); - - if (!fieldMeta[fieldName]) { - fieldMeta[fieldName] = {}; - } - - if (rangeMeta) { - fieldMeta[fieldName].range = rangeMeta; - } - - if (categoryMeta) { - fieldMeta[fieldName].categories = categoryMeta; - } - }); - - const metadataFeature: TileMetaFeature = { - type: 'Feature', - properties: { - [KBN_METADATA_FEATURE]: true, - [KBN_IS_TILE_COMPLETE]: true, - [KBN_VECTOR_SHAPE_TYPE_COUNTS]: counts, - [KBN_FEATURE_COUNT]: features.length, - fieldMeta, - }, - geometry: esBboxToGeoJsonPolygon(tileToESBbox(x, y, z), tileToESBbox(x, y, z)), - }; - - features.push(metadataFeature); - } - - const featureCollection: FeatureCollection = { - features, - type: 'FeatureCollection', - }; - - return createMvtTile(featureCollection, z, x, y); - } catch (e) { - if (!isAbortError(e)) { - logger.warn(`Cannot generate tile for ${z}/${x}/${y}: ${e.message}`); - } - return null; - } -} - -function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown { - const tileExtent = { - minLon: tileBounds.top_left.lon, - minLat: tileBounds.bottom_right.lat, - maxLon: tileBounds.bottom_right.lon, - maxLat: tileBounds.top_left.lat, - }; - const tileExtentFilter = createExtentFilter(tileExtent, [geometryFieldName]); - return tileExtentFilter.query; -} - -function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon { - // Intersecting geo_shapes may push bounding box outside of tile so need to clamp to tile bounds. - let minLon = Math.max(esBounds.top_left.lon, tileBounds.top_left.lon); - const maxLon = Math.min(esBounds.bottom_right.lon, tileBounds.bottom_right.lon); - minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline - const minLat = Math.max(esBounds.bottom_right.lat, tileBounds.bottom_right.lat); - const maxLat = Math.min(esBounds.top_left.lat, tileBounds.top_left.lat); - - return { - type: 'Polygon', - coordinates: [ - [ - [minLon, minLat], - [minLon, maxLat], - [maxLon, maxLat], - [maxLon, minLat], - [minLon, minLat], - ], - ], - }; -} - -function createMvtTile( - featureCollection: FeatureCollection, - z: number, - x: number, - y: number -): Buffer | null { - featureCollection.features.push(...getCentroidFeatures(featureCollection)); - const tileIndex = geojsonvt(featureCollection, { - maxZoom: 24, // max zoom to preserve detail on; can't be higher than 24 - tolerance: 3, // simplification tolerance (higher means simpler) - extent: 4096, // tile extent (both width and height) - buffer: 64, // tile buffer on each side - debug: 0, // logging level (0 to disable, 1 or 2) - lineMetrics: false, // whether to enable line metrics tracking for LineString/MultiLineString features - promoteId: null, // name of a feature property to promote to feature.id. Cannot be used with `generateId` - generateId: false, // whether to generate feature ids. Cannot be used with `promoteId` - indexMaxZoom: 5, // max zoom in the initial tile index - indexMaxPoints: 100000, // max number of points per tile in the index - }); - const tile = tileIndex.getTile(z, x, y); - - if (tile) { - const pbf = vtpbf.fromGeojsonVt({ [MVT_SOURCE_LAYER_NAME]: tile }, { version: 2 }); - return Buffer.from(pbf); - } else { - return null; - } -} diff --git a/x-pack/plugins/maps/server/mvt/mvt_routes.ts b/x-pack/plugins/maps/server/mvt/mvt_routes.ts index 01a89aff1a661..3c61a47a383d6 100644 --- a/x-pack/plugins/maps/server/mvt/mvt_routes.ts +++ b/x-pack/plugins/maps/server/mvt/mvt_routes.ts @@ -16,10 +16,10 @@ import { MVT_GETTILE_API_PATH, API_ROOT_PATH, MVT_GETGRIDTILE_API_PATH, - ES_GEO_FIELD_TYPE, RENDER_AS, } from '../../common/constants'; -import { getGridTile, getTile } from './get_tile'; +import { getEsTile } from './get_tile'; +import { getEsGridTile } from './get_grid_tile'; const CACHE_TIMEOUT_SECONDS = 60 * 60; @@ -43,8 +43,6 @@ export function initMVTRoutes({ geometryFieldName: schema.string(), requestBody: schema.string(), index: schema.string(), - geoFieldType: schema.string(), - searchSessionId: schema.maybe(schema.string()), token: schema.maybe(schema.string()), }), }, @@ -56,14 +54,15 @@ export function initMVTRoutes({ ) => { const { query, params } = request; - const abortController = new AbortController(); - request.events.aborted$.subscribe(() => { - abortController.abort(); - }); + // todo - replace with direct abortion of raw transport request + // const abortController = new AbortController(); + // request.events.aborted$.subscribe(() => { + // abortController.abort(); + // }); const requestBodyDSL = rison.decode(query.requestBody as string); - const tile = await getTile({ + const tile = await getEsTile({ logger, context, geometryFieldName: query.geometryFieldName as string, @@ -72,9 +71,6 @@ export function initMVTRoutes({ z: parseInt((params as any).z, 10) as number, index: query.index as string, requestBody: requestBodyDSL as any, - geoFieldType: query.geoFieldType as ES_GEO_FIELD_TYPE, - searchSessionId: query.searchSessionId, - abortSignal: abortController.signal, }); return sendResponse(response, tile); @@ -95,8 +91,6 @@ export function initMVTRoutes({ requestBody: schema.string(), index: schema.string(), requestType: schema.string(), - geoFieldType: schema.string(), - searchSessionId: schema.maybe(schema.string()), token: schema.maybe(schema.string()), }), }, @@ -107,14 +101,16 @@ export function initMVTRoutes({ response: KibanaResponseFactory ) => { const { query, params } = request; - const abortController = new AbortController(); - request.events.aborted$.subscribe(() => { - abortController.abort(); - }); + + // todo - replace with direct abortion of raw transport request + // const abortController = new AbortController(); + // request.events.aborted$.subscribe(() => { + // abortController.abort(); + // }); const requestBodyDSL = rison.decode(query.requestBody as string); - const tile = await getGridTile({ + const tile = await getEsGridTile({ logger, context, geometryFieldName: query.geometryFieldName as string, @@ -124,9 +120,6 @@ export function initMVTRoutes({ index: query.index as string, requestBody: requestBodyDSL as any, requestType: query.requestType as RENDER_AS.POINT | RENDER_AS.GRID, - geoFieldType: query.geoFieldType as ES_GEO_FIELD_TYPE, - searchSessionId: query.searchSessionId, - abortSignal: abortController.signal, }); return sendResponse(response, tile); diff --git a/x-pack/plugins/maps/server/mvt/util.ts b/x-pack/plugins/maps/server/mvt/util.ts deleted file mode 100644 index b3dc606ba3f11..0000000000000 --- a/x-pack/plugins/maps/server/mvt/util.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// This implementation: -// - does not include meta-fields -// - does not validate the schema against the index-pattern (e.g. nested fields) -// In the context of .mvt this is sufficient: -// - only fields from the response are packed in the tile (more efficient) -// - query-dsl submitted from the client, which was generated by the IndexPattern -// todo: Ideally, this should adapt/reuse from https://github.com/elastic/kibana/blob/52b42a81faa9dd5c102b9fbb9a645748c3623121/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts#L26 - -export function flattenHit( - geometryField: string, - hit: Record -): Record { - const flat: Record = {}; - if (hit) { - flattenSource(flat, '', hit._source as Record, geometryField); - if (hit.fields) { - flattenFields(flat, hit.fields as Array>); - } - - // Attach meta fields - flat._index = hit._index; - flat._id = hit._id; - } - return flat; -} - -function flattenSource( - accum: Record, - path: string, - properties: Record = {}, - geometryField: string -): Record { - accum = accum || {}; - for (const key in properties) { - if (properties.hasOwnProperty(key)) { - const newKey = path ? path + '.' + key : key; - let value; - if (geometryField === newKey) { - value = properties[key]; // do not deep-copy the geometry - } else if (properties[key] !== null && typeof value === 'object' && !Array.isArray(value)) { - value = flattenSource( - accum, - newKey, - properties[key] as Record, - geometryField - ); - } else { - value = properties[key]; - } - accum[newKey] = value; - } - } - return accum; -} - -function flattenFields(accum: Record = {}, fields: Array>) { - accum = accum || {}; - for (const key in fields) { - if (fields.hasOwnProperty(key)) { - const value = fields[key]; - if (Array.isArray(value)) { - accum[key] = value[0]; - } else { - accum[key] = value; - } - } - } -} diff --git a/x-pack/test/api_integration/apis/maps/get_grid_tile.js b/x-pack/test/api_integration/apis/maps/get_grid_tile.js index fdb8b2187bbbb..c37dc9770693c 100644 --- a/x-pack/test/api_integration/apis/maps/get_grid_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_grid_tile.js @@ -8,10 +8,6 @@ import { VectorTile } from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import expect from '@kbn/expect'; -import { - KBN_IS_CENTROID_FEATURE, - MVT_SOURCE_LAYER_NAME, -} from '../../../../plugins/maps/common/constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -23,45 +19,53 @@ export default function ({ getService }) { `/api/maps/mvt/getGridTile/3/2/3.pbf\ ?geometryFieldName=geo.coordinates\ &index=logstash-*\ -&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(avg_of_bytes:(avg:(field:bytes)),gridCentroid:(geo_centroid:(field:geo.coordinates))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\ -&requestType=point\ -&geoFieldType=geo_point` +&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\ +&requestType=point` ) .set('kbn-xsrf', 'kibana') .responseType('blob') .expect(200); const jsonTile = new VectorTile(new Protobuf(resp.body)); - const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME]; - expect(layer.length).to.be(2); // Cluster feature + const layer = jsonTile.layers.aggs; + expect(layer.length).to.be(1); const clusterFeature = layer.feature(0); expect(clusterFeature.type).to.be(1); expect(clusterFeature.extent).to.be(4096); expect(clusterFeature.id).to.be(undefined); - expect(clusterFeature.properties).to.eql({ doc_count: 1, avg_of_bytes: 9252 }); + expect(clusterFeature.properties).to.eql({ _count: 1, 'avg_of_bytes.value': 9252 }); expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]); // Metadata feature - const metadataFeature = layer.feature(1); + const metaDataLayer = jsonTile.layers.meta; + expect(metaDataLayer.length).to.be(1); + const metadataFeature = metaDataLayer.feature(0); expect(metadataFeature.type).to.be(3); expect(metadataFeature.extent).to.be(4096); - expect(metadataFeature.properties).to.eql({ - __kbn_metadata_feature__: true, - __kbn_feature_count__: 1, - __kbn_is_tile_complete__: true, - __kbn_vector_shape_type_counts__: '{"POINT":1,"LINE":0,"POLYGON":0}', - fieldMeta: - '{"doc_count":{"range":{"min":1,"max":1,"delta":0},"categories":{"categories":[{"key":1,"count":1}]}},"avg_of_bytes":{"range":{"min":9252,"max":9252,"delta":0},"categories":{"categories":[{"key":9252,"count":1}]}}}', - }); + + expect(metadataFeature.properties['aggregations._count.avg']).to.eql(1); + expect(metadataFeature.properties['aggregations._count.count']).to.eql(1); + expect(metadataFeature.properties['aggregations._count.min']).to.eql(1); + expect(metadataFeature.properties['aggregations._count.sum']).to.eql(1); + + expect(metadataFeature.properties['aggregations.avg_of_bytes.avg']).to.eql(9252); + expect(metadataFeature.properties['aggregations.avg_of_bytes.count']).to.eql(1); + expect(metadataFeature.properties['aggregations.avg_of_bytes.max']).to.eql(9252); + expect(metadataFeature.properties['aggregations.avg_of_bytes.min']).to.eql(9252); + expect(metadataFeature.properties['aggregations.avg_of_bytes.sum']).to.eql(9252); + + expect(metadataFeature.properties['hits.total.relation']).to.eql('eq'); + expect(metadataFeature.properties['hits.total.value']).to.eql(1); + expect(metadataFeature.loadGeometry()).to.eql([ [ - { x: 0, y: 0 }, - { x: 4096, y: 0 }, - { x: 4096, y: 4096 }, { x: 0, y: 4096 }, + { x: 4096, y: 4096 }, + { x: 4096, y: 0 }, { x: 0, y: 0 }, + { x: 0, y: 4096 }, ], ]); }); @@ -72,65 +76,62 @@ export default function ({ getService }) { `/api/maps/mvt/getGridTile/3/2/3.pbf\ ?geometryFieldName=geo.coordinates\ &index=logstash-*\ -&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(avg_of_bytes:(avg:(field:bytes)),gridCentroid:(geo_centroid:(field:geo.coordinates))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\ -&requestType=grid\ -&geoFieldType=geo_point` +&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\ +&requestType=grid` ) .set('kbn-xsrf', 'kibana') .responseType('blob') .expect(200); const jsonTile = new VectorTile(new Protobuf(resp.body)); - const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME]; - expect(layer.length).to.be(3); + const layer = jsonTile.layers.aggs; + expect(layer.length).to.be(1); const gridFeature = layer.feature(0); expect(gridFeature.type).to.be(3); expect(gridFeature.extent).to.be(4096); expect(gridFeature.id).to.be(undefined); - expect(gridFeature.properties).to.eql({ doc_count: 1, avg_of_bytes: 9252 }); + expect(gridFeature.properties).to.eql({ _count: 1, 'avg_of_bytes.value': 9252 }); expect(gridFeature.loadGeometry()).to.eql([ [ - { x: 96, y: 640 }, - { x: 96, y: 672 }, { x: 64, y: 672 }, - { x: 64, y: 640 }, + { x: 96, y: 672 }, { x: 96, y: 640 }, + { x: 64, y: 640 }, + { x: 64, y: 672 }, ], ]); // Metadata feature - const metadataFeature = layer.feature(1); + const metaDataLayer = jsonTile.layers.meta; + expect(metaDataLayer.length).to.be(1); + const metadataFeature = metaDataLayer.feature(0); expect(metadataFeature.type).to.be(3); expect(metadataFeature.extent).to.be(4096); - expect(metadataFeature.properties).to.eql({ - __kbn_metadata_feature__: true, - __kbn_feature_count__: 1, - __kbn_is_tile_complete__: true, - __kbn_vector_shape_type_counts__: '{"POINT":0,"LINE":0,"POLYGON":1}', - fieldMeta: - '{"doc_count":{"range":{"min":1,"max":1,"delta":0},"categories":{"categories":[{"key":1,"count":1}]}},"avg_of_bytes":{"range":{"min":9252,"max":9252,"delta":0},"categories":{"categories":[{"key":9252,"count":1}]}}}', - }); + + expect(metadataFeature.properties['aggregations._count.avg']).to.eql(1); + expect(metadataFeature.properties['aggregations._count.count']).to.eql(1); + expect(metadataFeature.properties['aggregations._count.min']).to.eql(1); + expect(metadataFeature.properties['aggregations._count.sum']).to.eql(1); + + expect(metadataFeature.properties['aggregations.avg_of_bytes.avg']).to.eql(9252); + expect(metadataFeature.properties['aggregations.avg_of_bytes.count']).to.eql(1); + expect(metadataFeature.properties['aggregations.avg_of_bytes.max']).to.eql(9252); + expect(metadataFeature.properties['aggregations.avg_of_bytes.min']).to.eql(9252); + expect(metadataFeature.properties['aggregations.avg_of_bytes.sum']).to.eql(9252); + + expect(metadataFeature.properties['hits.total.relation']).to.eql('eq'); + expect(metadataFeature.properties['hits.total.value']).to.eql(1); + expect(metadataFeature.loadGeometry()).to.eql([ [ - { x: 0, y: 0 }, - { x: 4096, y: 0 }, - { x: 4096, y: 4096 }, { x: 0, y: 4096 }, + { x: 4096, y: 4096 }, + { x: 4096, y: 0 }, { x: 0, y: 0 }, + { x: 0, y: 4096 }, ], ]); - - const clusterFeature = layer.feature(2); - expect(clusterFeature.type).to.be(1); - expect(clusterFeature.extent).to.be(4096); - expect(clusterFeature.id).to.be(undefined); - expect(clusterFeature.properties).to.eql({ - doc_count: 1, - avg_of_bytes: 9252, - [KBN_IS_CENTROID_FEATURE]: true, - }); - expect(clusterFeature.loadGeometry()).to.eql([[{ x: 80, y: 656 }]]); }); }); } diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index 9705064464843..699ff145aa1b1 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -8,7 +8,6 @@ import { VectorTile } from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import expect from '@kbn/expect'; -import { MVT_SOURCE_LAYER_NAME } from '../../../../plugins/maps/common/constants'; function findFeature(layer, callbackFn) { for (let i = 0; i < layer.length; i++) { @@ -23,22 +22,21 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('getTile', () => { - it('should return vector tile containing document', async () => { + it('should return ES vector tile containing documents and metadata', async () => { const resp = await supertest .get( `/api/maps/mvt/getTile/2/1/1.pbf\ ?geometryFieldName=geo.coordinates\ &index=logstash-*\ -&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw))\ -&geoFieldType=geo_point` +&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw))` ) .set('kbn-xsrf', 'kibana') .responseType('blob') .expect(200); const jsonTile = new VectorTile(new Protobuf(resp.body)); - const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME]; - expect(layer.length).to.be(3); // 2 docs + the metadata feature + const layer = jsonTile.layers.hits; + expect(layer.length).to.be(2); // 2 docs // Verify ES document @@ -50,82 +48,32 @@ export default function ({ getService }) { expect(feature.extent).to.be(4096); expect(feature.id).to.be(undefined); expect(feature.properties).to.eql({ - __kbn__feature_id__: 'logstash-2015.09.20:AU_x3_BsGFA8no6Qjjug:0', _id: 'AU_x3_BsGFA8no6Qjjug', _index: 'logstash-2015.09.20', bytes: 9252, - ['machine.os.raw']: 'ios', + 'machine.os.raw': 'ios', }); expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]); // Verify metadata feature - const metadataFeature = findFeature(layer, (feature) => { - return feature.properties.__kbn_metadata_feature__; - }); + const metaDataLayer = jsonTile.layers.meta; + const metadataFeature = metaDataLayer.feature(0); expect(metadataFeature).not.to.be(undefined); expect(metadataFeature.type).to.be(3); expect(metadataFeature.extent).to.be(4096); expect(metadataFeature.id).to.be(undefined); - const fieldMeta = JSON.parse(metadataFeature.properties.fieldMeta); - delete metadataFeature.properties.fieldMeta; - expect(metadataFeature.properties).to.eql({ - __kbn_feature_count__: 2, - __kbn_is_tile_complete__: true, - __kbn_metadata_feature__: true, - __kbn_vector_shape_type_counts__: '{"POINT":2,"LINE":0,"POLYGON":0}', - }); - expect(fieldMeta.bytes.range).to.eql({ - min: 9252, - max: 9583, - delta: 331, - }); - expect(fieldMeta.bytes.categories.categories.length).to.be(2); - expect(fieldMeta['machine.os.raw'].categories.categories.length).to.be(2); - expect(metadataFeature.loadGeometry()).to.eql([ - [ - { x: 0, y: 4096 }, - { x: 0, y: 0 }, - { x: 4096, y: 0 }, - { x: 4096, y: 4096 }, - { x: 0, y: 4096 }, - ], - ]); - }); - it('should return vector tile containing bounds when count exceeds size', async () => { - const resp = await supertest - // requestBody sets size=1 to force count exceeded - .get( - `/api/maps/mvt/getTile/2/1/1.pbf\ -?geometryFieldName=geo.coordinates\ -&index=logstash-*\ -&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:1,stored_fields:!(bytes,geo.coordinates,machine.os.raw))\ -&geoFieldType=geo_point` - ) - .set('kbn-xsrf', 'kibana') - .responseType('blob') - .expect(200); + // This is dropping some irrelevant properties from the comparison + expect(metadataFeature.properties['hits.total.relation']).to.eql('eq'); + expect(metadataFeature.properties['hits.total.value']).to.eql(2); + expect(metadataFeature.properties.timed_out).to.eql(false); - const jsonTile = new VectorTile(new Protobuf(resp.body)); - const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME]; - expect(layer.length).to.be(1); - - const metadataFeature = layer.feature(0); - expect(metadataFeature.type).to.be(3); - expect(metadataFeature.extent).to.be(4096); - expect(metadataFeature.id).to.be(undefined); - expect(metadataFeature.properties).to.eql({ - __kbn_metadata_feature__: true, - __kbn_feature_count__: 0, - __kbn_is_tile_complete__: false, - __kbn_vector_shape_type_counts__: '{"POINT":0,"LINE":0,"POLYGON":0}', - }); expect(metadataFeature.loadGeometry()).to.eql([ [ { x: 44, y: 2382 }, - { x: 44, y: 1913 }, - { x: 550, y: 1913 }, { x: 550, y: 2382 }, + { x: 550, y: 1913 }, + { x: 44, y: 1913 }, { x: 44, y: 2382 }, ], ]); diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 58c69950590cf..471e7440822c5 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -6,10 +6,6 @@ */ import expect from '@kbn/expect'; -import { - KBN_IS_TILE_COMPLETE, - KBN_METADATA_FEATURE, -} from '../../../../plugins/maps/common/constants'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -44,7 +40,6 @@ export default function ({ getPageObjects, getService }) { maxzoom: 24, filter: [ 'all', - ['!=', ['get', '__kbn_metadata_feature__'], true], ['!=', ['get', '__kbn_is_centroid_feature__'], true], ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], ['==', ['get', '__kbn_isvisibleduetojoin__'], true], @@ -125,7 +120,6 @@ export default function ({ getPageObjects, getService }) { maxzoom: 24, filter: [ 'all', - ['!=', ['get', '__kbn_metadata_feature__'], true], ['!=', ['get', '__kbn_is_centroid_feature__'], true], ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], ['==', ['get', '__kbn_isvisibleduetojoin__'], true], @@ -202,7 +196,6 @@ export default function ({ getPageObjects, getService }) { maxzoom: 24, filter: [ 'all', - ['!=', ['get', '__kbn_metadata_feature__'], true], ['!=', ['get', '__kbn_is_centroid_feature__'], true], [ 'any', @@ -217,26 +210,5 @@ export default function ({ getPageObjects, getService }) { paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, }); }); - - it('should style incomplete data layer as expected', async () => { - const layer = mapboxStyle.layers.find((mbLayer) => { - return mbLayer.id === 'n1t6f_toomanyfeatures'; - }); - - expect(layer).to.eql({ - id: 'n1t6f_toomanyfeatures', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', KBN_METADATA_FEATURE], true], - ['==', ['get', KBN_IS_TILE_COMPLETE], false], - ], - layout: { visibility: 'visible' }, - paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, - }); - }); }); } diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js index 66a511f6e9fec..d9b660ba0d730 100644 --- a/x-pack/test/functional/apps/maps/mvt_scaling.js +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect( mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0].startsWith( - `/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape` + `/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))` ) ).to.equal(true); @@ -77,5 +77,34 @@ export default function ({ getPageObjects, getService }) { 'fill-opacity': 1, }); }); + + it('Style should include toomanyfeatures layer', async () => { + const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === `${VECTOR_SOURCE_ID}_toomanyfeatures`; + }); + + expect(layer).to.eql({ + id: 'caffa63a-ebfb-466d-8ff6-d797975b88ab_toomanyfeatures', + type: 'line', + source: 'caffa63a-ebfb-466d-8ff6-d797975b88ab', + 'source-layer': 'meta', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', 'hits.total.relation'], 'gte'], + ['>=', ['get', 'hits.total.value'], 10002], + ], + layout: { visibility: 'visible' }, + paint: { + 'line-color': '#fec514', + 'line-width': 3, + 'line-dasharray': [2, 1], + 'line-opacity': 1, + }, + }); + }); }); } diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index dcd2923cb9335..6c5065a77c1d2 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect( mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0].startsWith( - `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point` + `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(max_of_bytes:(max:(field:bytes))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid` ) ).to.equal(true); @@ -51,9 +51,9 @@ export default function ({ getPageObjects, getService }) { 'coalesce', [ 'case', - ['==', ['get', 'max_of_bytes'], null], + ['==', ['get', 'max_of_bytes.value'], null], 1622, - ['max', ['min', ['to-number', ['get', 'max_of_bytes']], 9790], 1623], + ['max', ['min', ['to-number', ['get', 'max_of_bytes.value']], 9790], 1623], ], 1622, ], From b1dab1f0299ccc4d7aaee494aab266f805ace2ff Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Mon, 25 Oct 2021 12:42:25 -0400 Subject: [PATCH 37/41] Suggestions enabled check (#116136) --- .../suggested_documents_callout.test.tsx | 15 ++++++- .../curation/suggested_documents_callout.tsx | 13 ++++-- .../curations/views/curations.test.tsx | 41 ++++++------------- .../components/curations/views/curations.tsx | 15 ++++--- .../views/curations_overview.test.tsx | 36 ++++------------ .../curations/views/curations_overview.tsx | 12 ++---- .../curations_settings_logic.test.ts | 16 ++++++++ .../curations_settings_logic.ts | 5 +++ .../components/engine/engine_logic.test.ts | 1 + .../app_search/components/engine/types.ts | 1 + .../suggested_curations_callout.test.tsx | 20 +++++---- .../suggested_curations_callout.tsx | 9 ++-- 12 files changed, 95 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.test.tsx index 29418d09218f4..b1f02b960aa8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.test.tsx @@ -26,6 +26,10 @@ const MOCK_VALUES = { }, queries: ['some query'], }, + // EngineLogic + engine: { + search_relevance_suggestions_active: true, + }, }; describe('SuggestedDocumentsCallout', () => { @@ -40,7 +44,7 @@ describe('SuggestedDocumentsCallout', () => { expect(wrapper.is(SuggestionsCallout)); }); - it('is empty when the suggested is undefined', () => { + it('is empty when the suggestion is undefined', () => { setMockValues({ ...MOCK_VALUES, curation: {} }); const wrapper = shallow(); @@ -48,6 +52,15 @@ describe('SuggestedDocumentsCallout', () => { expect(wrapper.isEmptyRender()).toBe(true); }); + it('is empty when suggestions are not active', () => { + const values = set('engine.search_relevance_suggestions_active', false, MOCK_VALUES); + setMockValues(values); + + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + it('is empty when curation status is not pending', () => { const values = set('curation.suggestion.status', 'applied', MOCK_VALUES); setMockValues(values); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx index e443e77d76190..af76ebee16bad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx @@ -11,7 +11,7 @@ import { useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { ENGINE_CURATION_SUGGESTION_PATH } from '../../../routes'; -import { generateEnginePath } from '../../engine'; +import { EngineLogic, generateEnginePath } from '../../engine'; import { SuggestionsCallout } from '../components/suggestions_callout'; @@ -21,8 +21,15 @@ export const SuggestedDocumentsCallout: React.FC = () => { const { curation: { suggestion, queries }, } = useValues(CurationLogic); - - if (typeof suggestion === 'undefined' || suggestion.status !== 'pending') { + const { + engine: { search_relevance_suggestions_active: searchRelevanceSuggestionsActive }, + } = useValues(EngineLogic); + + if ( + typeof suggestion === 'undefined' || + suggestion.status !== 'pending' || + searchRelevanceSuggestionsActive === false + ) { return null; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index 4e09dadc6c836..49d48c8c05ba6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -14,6 +14,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { set } from 'lodash/fp'; + import { EuiTab } from '@elastic/eui'; import { getPageHeaderTabs, getPageTitle } from '../../../../test_helpers'; @@ -51,8 +53,10 @@ describe('Curations', () => { curationsSettings: { enabled: true, }, - // LicensingLogic - hasPlatinumLicense: true, + // EngineLogic + engine: { + search_relevance_suggestions_active: true, + }, }; const actions = { @@ -84,8 +88,8 @@ describe('Curations', () => { expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(3, 'settings'); }); - it('renders less tabs when less than platinum license', () => { - setMockValues({ ...values, hasPlatinumLicense: false }); + it('renders less tabs when suggestions are not active', () => { + setMockValues(set('engine.search_relevance_suggestions_active', false, values)); const wrapper = shallow(); expect(getPageTitle(wrapper)).toEqual('Curated results'); @@ -94,8 +98,8 @@ describe('Curations', () => { expect(tabs.length).toBe(2); }); - it('renders a New! badge when less than platinum license', () => { - setMockValues({ ...values, hasPlatinumLicense: false }); + it('renders a New! badge when suggestions are not active', () => { + setMockValues(set('engine.search_relevance_suggestions_active', false, values)); const wrapper = shallow(); expect(getPageTitle(wrapper)).toEqual('Curated results'); @@ -104,29 +108,8 @@ describe('Curations', () => { expect(tabs.at(1).prop('append')).not.toBeUndefined(); }); - it('renders a New! badge when suggestions are disabled', () => { - setMockValues({ - ...values, - curationsSettings: { - enabled: false, - }, - }); - const wrapper = shallow(); - - expect(getPageTitle(wrapper)).toEqual('Curated results'); - - const tabs = getPageHeaderTabs(wrapper).find(EuiTab); - expect(tabs.at(2).prop('append')).not.toBeUndefined(); - }); - - it('hides the badge when suggestions are enabled and the user has a platinum license', () => { - setMockValues({ - ...values, - hasPlatinumLicense: true, - curationsSettings: { - enabled: true, - }, - }); + it('hides the badge when suggestions are active', () => { + setMockValues(set('engine.search_relevance_suggestions_active', true, values)); const wrapper = shallow(); expect(getPageTitle(wrapper)).toEqual('Curated results'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 1cd8313743536..2207555772b5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -12,11 +12,10 @@ import { useValues, useActions } from 'kea'; import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { LicensingLogic } from '../../../../shared/licensing'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { ENGINE_CURATIONS_NEW_PATH } from '../../../routes'; -import { generateEnginePath } from '../../engine'; +import { EngineLogic, generateEnginePath } from '../../engine'; import { AppSearchPageTemplate } from '../../layout'; import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants'; @@ -30,13 +29,13 @@ import { CurationsSettings, CurationsSettingsLogic } from './curations_settings' export const Curations: React.FC = () => { const { dataLoading: curationsDataLoading, meta, selectedPageTab } = useValues(CurationsLogic); const { loadCurations, onSelectPageTab } = useActions(CurationsLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); const { - dataLoading: curationsSettingsDataLoading, - curationsSettings: { enabled: curationsSettingsEnabled }, - } = useValues(CurationsSettingsLogic); + engine: { search_relevance_suggestions_active: searchRelevanceSuggestionsActive }, + } = useValues(EngineLogic); - const suggestionsEnabled = hasPlatinumLicense && curationsSettingsEnabled; + const { dataLoading: curationsSettingsDataLoading } = useValues(CurationsSettingsLogic); + + const suggestionsEnabled = searchRelevanceSuggestionsActive; const OVERVIEW_TAB = { label: i18n.translate( @@ -75,7 +74,7 @@ export const Curations: React.FC = () => { ), }; - const pageTabs = hasPlatinumLicense + const pageTabs = searchRelevanceSuggestionsActive ? [OVERVIEW_TAB, HISTORY_TAB, SETTINGS_TAB] : [OVERVIEW_TAB, SETTINGS_TAB]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx index 809157704a14e..43ef9dfd7ad2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx @@ -12,6 +12,7 @@ import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; +import { set } from 'lodash/fp'; import { CurationsTable, EmptyState } from '../components'; @@ -33,8 +34,10 @@ const MOCK_VALUES = { id: 'cur-id-2', }, ], - // LicensingLogics - hasPlatinumLicense: true, + // EngineLogic + engine: { + search_relevance_suggestions_active: true, + }, }; describe('CurationsOverview', () => { @@ -67,36 +70,15 @@ describe('CurationsOverview', () => { expect(wrapper.find(CurationsTable)).toHaveLength(1); }); - it('renders a suggestions table when the user has a platinum license and curations suggestions enabled', () => { - setMockValues({ - ...MOCK_VALUES, - hasPlatinumLicense: true, - curationsSettings: { - enabled: true, - }, - }); + it('renders a suggestions table when suggestions are active', () => { + setMockValues(set('engine.search_relevance_suggestions_active', true, MOCK_VALUES)); const wrapper = shallow(); expect(wrapper.find(SuggestionsTable).exists()).toBe(true); }); - it('doesn\t render a suggestions table when the user has no platinum license', () => { - setMockValues({ - ...MOCK_VALUES, - hasPlatinumLicense: false, - }); - const wrapper = shallow(); - - expect(wrapper.find(SuggestionsTable).exists()).toBe(false); - }); - - it('doesn\t render a suggestions table when the user has disabled suggestions', () => { - setMockValues({ - ...MOCK_VALUES, - curationsSettings: { - enabled: false, - }, - }); + it('doesn\t render a suggestions table when suggestions are not active', () => { + setMockValues(set('engine.search_relevance_suggestions_active', false, MOCK_VALUES)); const wrapper = shallow(); expect(wrapper.find(SuggestionsTable).exists()).toBe(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx index 00593403b08cf..a611ca88cefd4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx @@ -11,22 +11,18 @@ import { useValues } from 'kea'; import { EuiSpacer } from '@elastic/eui'; -import { LicensingLogic } from '../../../../shared/licensing'; +import { EngineLogic } from '../../engine'; import { CurationsTable, EmptyState } from '../components'; import { SuggestionsTable } from '../components/suggestions_table'; import { CurationsLogic } from '../curations_logic'; -import { CurationsSettingsLogic } from './curations_settings'; - export const CurationsOverview: React.FC = () => { const { curations } = useValues(CurationsLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); - const { - curationsSettings: { enabled }, - } = useValues(CurationsSettingsLogic); + engine: { search_relevance_suggestions_active: searchRelevanceSuggestionsActive }, + } = useValues(EngineLogic); - const shouldShowSuggestions = enabled && hasPlatinumLicense; + const shouldShowSuggestions = searchRelevanceSuggestionsActive; return ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts index 818fac3d0706e..b8aae9c39174d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts @@ -12,8 +12,20 @@ import { } from '../../../../../__mocks__/kea_logic'; import '../../../../__mocks__/engine_logic.mock'; +jest.mock('../../curations_logic', () => ({ + CurationsLogic: { + values: {}, + actions: { + loadCurations: jest.fn(), + }, + }, +})); + import { nextTick } from '@kbn/test/jest'; +import { CurationsLogic } from '../..'; +import { EngineLogic } from '../../../engine'; + import { CurationsSettingsLogic } from './curations_settings_logic'; const DEFAULT_VALUES = { @@ -205,6 +217,10 @@ describe('CurationsSettingsLogic', () => { enabled: true, mode: 'automatic', }); + + // data should have been reloaded + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalled(); + expect(CurationsLogic.actions.loadCurations).toHaveBeenCalled(); }); it('presents any API errors to the user', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts index d79ad64a69788..3984cbd024da4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.ts @@ -10,6 +10,7 @@ import { kea, MakeLogicType } from 'kea'; import { flashAPIErrors } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; import { EngineLogic } from '../../../engine'; +import { CurationsLogic } from '../../curations_logic'; export interface CurationsSettings { enabled: boolean; @@ -101,6 +102,10 @@ export const CurationsSettingsLogic = kea< } ); actions.onCurationsSettingsLoad(response.curation); + + // Re-fetch data so that UI updates to new settings + CurationsLogic.actions.loadCurations(); + EngineLogic.actions.initializeEngine(); } catch (e) { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index 28739a2799332..bb30190833dd3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -43,6 +43,7 @@ describe('EngineLogic', () => { schema: { test: SchemaType.Text }, apiTokens: [], apiKey: 'some-key', + search_relevance_suggestions_active: true, }; const DEFAULT_VALUES = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index d4c652ab9c7a7..0bfbc185b85f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -54,6 +54,7 @@ export interface EngineDetails extends Engine { engine_count?: number; includedEngines?: EngineDetails[]; search_relevance_suggestions?: SearchRelevanceSuggestionDetails; + search_relevance_suggestions_active: boolean; } interface ResultField { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.test.tsx index 58e2cd8cf4c9b..c65d95a5254ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.test.tsx @@ -18,16 +18,14 @@ import { SuggestionsCallout } from '../../curations/components/suggestions_callo import { SuggestedCurationsCallout } from './suggested_curations_callout'; const MOCK_VALUES = { - // EngineLogic engine: { search_relevance_suggestions: { curation: { pending: 1, }, }, + search_relevance_suggestions_active: true, }, - // LicensingLogic - hasPlatinumLicense: true, }; describe('SuggestedCurationsCallout', () => { @@ -43,15 +41,20 @@ describe('SuggestedCurationsCallout', () => { }); it('is empty when the suggestions are undefined', () => { - setMockValues({ ...MOCK_VALUES, engine: {} }); + setMockValues({ + ...MOCK_VALUES, + engine: { + search_relevance_suggestions_active: true, + }, + }); const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBe(true); }); - it('is empty when no pending curations', () => { - const values = set('engine.search_relevance_suggestions.curation.pending', 0, MOCK_VALUES); + it('is empty when suggestions are not active', () => { + const values = set('engine.search_relevance_suggestions_active', false, MOCK_VALUES); setMockValues(values); const wrapper = shallow(); @@ -59,9 +62,8 @@ describe('SuggestedCurationsCallout', () => { expect(wrapper.isEmptyRender()).toBe(true); }); - it('is empty when the user has no platinum license', () => { - // This would happen if the user *had* suggestions and then downgraded from platinum to gold or something - const values = set('hasPlatinumLicense', false, MOCK_VALUES); + it('is empty when no pending curations', () => { + const values = set('engine.search_relevance_suggestions.curation.pending', 0, MOCK_VALUES); setMockValues(values); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx index 046cc2d744b00..04b2d2b207e94 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/suggested_curations_callout.tsx @@ -10,23 +10,24 @@ import { useValues } from 'kea'; import { i18n } from '@kbn/i18n'; -import { LicensingLogic } from '../../../../shared/licensing'; import { ENGINE_CURATIONS_PATH } from '../../../routes'; import { SuggestionsCallout } from '../../curations/components/suggestions_callout'; import { EngineLogic, generateEnginePath } from '../../engine'; export const SuggestedCurationsCallout: React.FC = () => { const { - engine: { search_relevance_suggestions: searchRelevanceSuggestions }, + engine: { + search_relevance_suggestions: searchRelevanceSuggestions, + search_relevance_suggestions_active: searchRelevanceSuggestionsActive, + }, } = useValues(EngineLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); const pendingCount = searchRelevanceSuggestions?.curation.pending; if ( typeof searchRelevanceSuggestions === 'undefined' || pendingCount === 0 || - hasPlatinumLicense === false + searchRelevanceSuggestionsActive === false ) { return null; } From 14ac1643e6d12ab37f4f347adcd623dc5495ac2e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 25 Oct 2021 19:51:26 +0300 Subject: [PATCH 38/41] [Cases] Add another newline after a quote message (#116104) --- .../components/add_comment/index.test.tsx | 34 +++++++++++- .../public/components/add_comment/index.tsx | 53 +++++++++++++++++-- .../components/markdown_editor/editor.tsx | 2 +- .../user_action_tree/index.test.tsx | 2 +- .../components/user_action_tree/index.tsx | 4 +- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 06a3897687921..c15722a3ec354 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -120,7 +120,7 @@ describe('AddComment ', () => { }); it('should insert a quote', async () => { - const sampleQuote = 'what a cool quote'; + const sampleQuote = 'what a cool quote \n with new lines'; const ref = React.createRef(); const wrapper = mount( @@ -138,10 +138,40 @@ describe('AddComment ', () => { }); expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe( - `${sampleData.comment}\n\n${sampleQuote}` + `${sampleData.comment}\n\n> what a cool quote \n> with new lines \n\n` ); }); + it('should call onFocus when adding a quote', async () => { + const ref = React.createRef(); + + mount( + + + + ); + + ref.current!.editor!.textarea!.focus = jest.fn(); + await act(async () => { + ref.current!.addQuote('a comment'); + }); + + expect(ref.current!.editor!.textarea!.focus).toHaveBeenCalled(); + }); + + it('should NOT call onFocus on mount', async () => { + const ref = React.createRef(); + + mount( + + + + ); + + ref.current!.editor!.textarea!.focus = jest.fn(); + expect(ref.current!.editor!.textarea!.focus).not.toHaveBeenCalled(); + }); + it('it should insert a timeline', async () => { const useInsertTimelineMock = jest.fn(); let attachTimeline = noop; diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index f788456a30dff..3ee7c1604b24d 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -5,14 +5,22 @@ * 2.0. */ +import React, { + useCallback, + useRef, + forwardRef, + useImperativeHandle, + useEffect, + useState, +} from 'react'; import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useRef, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; +import { isEmpty } from 'lodash'; import { CommentType } from '../../../common'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; -import { MarkdownEditorForm } from '../markdown_editor'; +import { EuiMarkdownEditorRef, MarkdownEditorForm } from '../markdown_editor'; import { Form, useForm, UseField, useFormData } from '../../common/shared_imports'; import * as i18n from './translations'; @@ -33,6 +41,7 @@ const initialCommentValue: AddCommentFormSchema = { export interface AddCommentRefObject { addQuote: (quote: string) => void; setComment: (newComment: string) => void; + editor: EuiMarkdownEditorRef | null; } export interface AddCommentProps { @@ -61,7 +70,8 @@ export const AddComment = React.memo( }, ref ) => { - const editorRef = useRef(); + const editorRef = useRef(null); + const [focusOnContext, setFocusOnContext] = useState(false); const owner = useOwnerContext(); const { isLoading, postComment } = usePostComment(); @@ -77,7 +87,10 @@ export const AddComment = React.memo( const addQuote = useCallback( (quote) => { - setFieldValue(fieldName, `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); + const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), '\n> '); + const val = `> ${addCarrots} \n\n`; + setFieldValue(fieldName, `${comment}${comment.length > 0 ? '\n\n' : ''}${val}`); + setFocusOnContext(true); }, [comment, setFieldValue] ); @@ -111,6 +124,38 @@ export const AddComment = React.memo( } }, [submit, onCommentSaving, postComment, caseId, owner, onCommentPosted, subCaseId, reset]); + /** + * Focus on the text area when a quote has been added. + * + * The useEffect will run only when focusOnContext + * changes. + * + * The useEffect is also called once one mount + * where the comment is empty. We do not want to focus + * in this scenario. + * + * Ideally we would like to put the + * editorRef.current?.textarea?.focus(); inside the if (focusOnContext). + * The reason this is not feasible is because when it sets the + * focusOnContext to false a render will occur again and the + * focus will be lost. + * + * We do not put the comment in the dependency list + * because we do not want to focus when the user + * is typing. + */ + + useEffect(() => { + if (!isEmpty(comment)) { + editorRef.current?.textarea?.focus(); + } + + if (focusOnContext) { + setFocusOnContext(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [focusOnContext]); + return ( {isLoading && showLoading && } diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx index f2351a2b2d793..4bf25b23403e1 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -37,7 +37,7 @@ interface MarkdownEditorProps { value: string; } -type EuiMarkdownEditorRef = ElementRef; +export type EuiMarkdownEditorRef = ElementRef; export interface MarkdownEditorRef { textarea: HTMLTextAreaElement | null; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx index dd5c993939b82..5241b0e66fb38 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx @@ -348,7 +348,7 @@ describe(`UserActionTree`, () => { .first() .simulate('click'); await waitFor(() => { - expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); + expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n\n`); }); }); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 7ea415324194c..92640a34548e8 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -226,10 +226,8 @@ export const UserActionTree = React.memo( const handleManageQuote = useCallback( (quote: string) => { - const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); - if (commentRefs.current[NEW_ID]) { - commentRefs.current[NEW_ID].addQuote(`> ${addCarrots} \n`); + commentRefs.current[NEW_ID].addQuote(quote); } handleOutlineComment('add-comment'); From 59815a06813a10a1fddbb8a762cdf651ad962c4b Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 25 Oct 2021 11:54:37 -0500 Subject: [PATCH 39/41] Upgrade EUI to v40.0.0 (#115639) * eui to v40.0.0 * tokenKeyword -> tokenTag * mobileOptions type * snapshot updates * Revert "tokenKeyword -> tokenTag" This reverts commit 0e5eae64e773ffa7e744de5da5e1c277423cd890. * token snapshot --- package.json | 2 +- .../collapsible_nav.test.tsx.snap | 315 +++++++++++++----- .../chrome/ui/header/collapsible_nav.test.tsx | 5 +- src/dev/license_checker/config.ts | 2 +- .../__snapshots__/agg.test.tsx.snap | 2 + .../value_axes_panel.test.tsx.snap | 4 + .../value_axis_options.test.tsx.snap | 2 + .../var_config.stories.storyshot | 29 +- .../curations/components/curations_table.tsx | 3 - .../components/tables/shared_columns.tsx | 3 - .../edit_layer_panel.test.tsx.snap | 2 + .../__snapshots__/ccr_shard.test.js.snap | 2 + ...screen_capture_panel_content.test.tsx.snap | 132 ++++++-- .../modal_all_errors.test.tsx.snap | 2 + .../__snapshots__/index.test.tsx.snap | 8 + .../__snapshots__/index.test.tsx.snap | 6 + .../__snapshots__/ping_headers.test.tsx.snap | 2 + yarn.lock | 8 +- 18 files changed, 391 insertions(+), 138 deletions(-) diff --git a/package.json b/package.json index 177b70efd4cc7..d9dd4912481b9 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.21", "@elastic/ems-client": "7.16.0", - "@elastic/eui": "39.1.1", + "@elastic/eui": "40.0.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 6987b779d5d45..571b564f90329 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -644,6 +644,11 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` > } + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -685,28 +692,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-kibana" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -941,28 +977,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-observability" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -1233,28 +1298,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-securitySolution" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -1486,28 +1580,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
} + buttonElement="button" className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-management" + element="div" id="generated-id" initialIsOpen={true} isLoading={false} @@ -1700,28 +1823,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + +
{ diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index efa54e74fdf2f..305eeb9a6a358 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -75,6 +75,6 @@ export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint '@elastic/ems-client@7.16.0': ['Elastic License 2.0'], - '@elastic/eui@39.1.1': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@40.0.0': ['SSPL-1.0 OR Elastic License 2.0'], 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry }; diff --git a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap index bc6d28bd5c1c4..b25444d16c46a 100644 --- a/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap +++ b/src/plugins/vis_default_editor/public/components/__snapshots__/agg.test.tsx.snap @@ -12,8 +12,10 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` } buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + buttonElement="button" className="visEditorSidebar__section visEditorSidebar__collapsible visEditorSidebar__collapsible--marginBottom" data-test-subj="visEditorAggAccordion1" + element="div" extraAction={
} buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + buttonElement="button" className="visEditorSidebar__section visEditorSidebar__collapsible" data-test-subj="toggleYAxisOptions-ValueAxis-1" + element="div" extraAction={ } buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + buttonElement="button" className="visEditorSidebar__section visEditorSidebar__collapsible" data-test-subj="toggleYAxisOptions-ValueAxis-2" + element="div" extraAction={ + + + + + + +
} + buttonElement="button" + element="div" id="responseHeaderAccord" initialIsOpen={false} isLoading={false} diff --git a/yarn.lock b/yarn.lock index 669f8321fb4fc..a36d8e9373685 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2412,10 +2412,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@39.1.1": - version "39.1.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-39.1.1.tgz#52e59f1dd6448b2e80047259ca60c6c87e9873f0" - integrity sha512-zYCNitpp6Ds7U6eaa9QkJqc20ZMo2wjpZokNtd1WalFV22vdfiVizFg7DMtDjJrCDLmoXcLOOCMasKlmmJ1cRg== +"@elastic/eui@40.0.0": + version "40.0.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-40.0.0.tgz#9556a87fa5eb7d9061e85f71ea9d3e6a9022dc3e" + integrity sha512-Zsz8eczEjthMgU00YhnsNmkKA8j4hxQpWNnrgecMgpcFEIj+Nn5WBofL/TJux/latS/mB4WWmrq4FTiSIyv/+Q== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From 62e203818fc1e2489bf03841ad470ff3e526ad7e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 25 Oct 2021 20:13:47 +0300 Subject: [PATCH 40/41] [Connectors][ServiceNow] Rename isLegacy configuration property (#115028) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../servicenow/api_sir.test.ts | 16 ++++----- .../servicenow/api_sir.ts | 14 ++++---- .../builtin_action_types/servicenow/schema.ts | 4 +-- .../servicenow/service.ts | 29 +++++++-------- .../saved_objects/actions_migrations.test.ts | 10 +++--- .../saved_objects/actions_migrations.ts | 6 ++-- .../configure_cases/connectors.test.tsx | 4 +-- .../components/configure_cases/connectors.tsx | 4 +-- .../connectors_dropdown.test.tsx | 8 ++--- .../configure_cases/connectors_dropdown.tsx | 6 ++-- .../connectors/deprecated_callout.test.tsx | 4 +-- .../connectors/deprecated_callout.tsx | 14 ++++---- .../servicenow_itsm_case_fields.test.tsx | 12 +++---- .../servicenow_sir_case_fields.test.tsx | 12 +++---- .../connectors/servicenow/validator.test.ts | 8 ++--- .../connectors/servicenow/validator.ts | 6 ++-- .../plugins/cases/public/components/utils.ts | 15 ++++++-- .../cases/public/containers/configure/mock.ts | 4 +-- .../servicenow/helpers.ts | 13 +++++-- .../servicenow/servicenow.test.tsx | 6 ++-- .../servicenow/servicenow.tsx | 2 +- .../servicenow/servicenow_connectors.test.tsx | 10 +++--- .../servicenow/servicenow_connectors.tsx | 35 ++++++++++--------- .../servicenow/servicenow_itsm_params.tsx | 8 ++--- .../servicenow/servicenow_sir_params.tsx | 8 ++--- .../builtin_action_types/servicenow/types.ts | 2 +- .../servicenow/update_connector.test.tsx | 2 +- .../servicenow/use_get_app_info.test.tsx | 2 +- .../components/actions_connectors_list.tsx | 6 ++-- .../builtin_action_types/servicenow_itsm.ts | 20 +++++------ .../builtin_action_types/servicenow_sir.ts | 20 +++++------ .../case_api_integration/common/lib/utils.ts | 4 +-- .../tests/trial/configure/get_connectors.ts | 4 +-- .../tests/trial/configure/get_connectors.ts | 4 +-- 34 files changed, 168 insertions(+), 154 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts index 358af7cd2e9ef..e5a161611fcb1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts @@ -132,7 +132,7 @@ describe('api_sir', () => { }); describe('prepareParams', () => { - test('it prepares the params correctly when the connector is legacy', async () => { + test('it prepares the params correctly when the connector uses the old API', async () => { expect(prepareParams(true, sirParams)).toEqual({ ...sirParams, incident: { @@ -145,7 +145,7 @@ describe('api_sir', () => { }); }); - test('it prepares the params correctly when the connector is not legacy', async () => { + test('it prepares the params correctly when the connector does not uses the old API', async () => { expect(prepareParams(false, sirParams)).toEqual({ ...sirParams, incident: { @@ -158,7 +158,7 @@ describe('api_sir', () => { }); }); - test('it prepares the params correctly when the connector is legacy and the observables are undefined', async () => { + test('it prepares the params correctly when the connector uses the old API and the observables are undefined', async () => { const { dest_ip: destIp, source_ip: sourceIp, @@ -192,7 +192,7 @@ describe('api_sir', () => { const res = await apiSIR.pushToService({ externalService, params, - config: { isLegacy: false }, + config: { usesTableApi: false }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -221,7 +221,7 @@ describe('api_sir', () => { await apiSIR.pushToService({ externalService, params, - config: { isLegacy: false }, + config: { usesTableApi: false }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -244,12 +244,12 @@ describe('api_sir', () => { ); }); - test('it does not call bulkAddObservableToIncident if it a legacy connector', async () => { + test('it does not call bulkAddObservableToIncident if the connector uses the old API', async () => { const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } }; await apiSIR.pushToService({ externalService, params, - config: { isLegacy: true }, + config: { usesTableApi: true }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -274,7 +274,7 @@ describe('api_sir', () => { await apiSIR.pushToService({ externalService, params, - config: { isLegacy: false }, + config: { usesTableApi: false }, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts index 326bb79a0e708..4e74d79c6f4a0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts @@ -59,13 +59,13 @@ const observablesToString = (obs: string | string[] | null | undefined): string }; export const prepareParams = ( - isLegacy: boolean, + usesTableApi: boolean, params: PushToServiceApiParamsSIR ): PushToServiceApiParamsSIR => { - if (isLegacy) { + if (usesTableApi) { /** * The schema has change to accept an array of observables - * or a string. In the case of a legacy connector we need to + * or a string. In the case of connector that uses the old API we need to * convert the observables to a string */ return { @@ -81,8 +81,8 @@ export const prepareParams = ( } /** - * For non legacy connectors the observables - * will be added in a different call. + * For connectors that do not use the old API + * the observables will be added in a different call. * They need to be set to null when sending the fields * to ServiceNow */ @@ -108,7 +108,7 @@ const pushToServiceHandler = async ({ }: PushToServiceApiHandlerArgs): Promise => { const res = await api.pushToService({ externalService, - params: prepareParams(!!config.isLegacy, params as PushToServiceApiParamsSIR), + params: prepareParams(!!config.usesTableApi, params as PushToServiceApiParamsSIR), config, secrets, commentFieldKey, @@ -130,7 +130,7 @@ const pushToServiceHandler = async ({ * through the pushToService call. */ - if (!config.isLegacy) { + if (!config.usesTableApi) { const sirExternalService = externalService as ExternalServiceSIR; const obsWithType: Array<[string[], ObservableTypes]> = [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index af8d1b9f38b17..e41eea24834c7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -14,7 +14,7 @@ export const ExternalIncidentServiceConfigurationBase = { export const ExternalIncidentServiceConfiguration = { ...ExternalIncidentServiceConfigurationBase, - isLegacy: schema.boolean({ defaultValue: true }), + usesTableApi: schema.boolean({ defaultValue: true }), }; export const ExternalIncidentServiceConfigurationBaseSchema = schema.object( @@ -49,7 +49,7 @@ const CommonAttributes = { externalId: schema.nullable(schema.string()), category: schema.nullable(schema.string()), subcategory: schema.nullable(schema.string()), - correlation_id: schema.nullable(schema.string()), + correlation_id: schema.nullable(schema.string({ defaultValue: DEFAULT_ALERTS_GROUPING_KEY })), correlation_display: schema.nullable(schema.string()), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index cb030c7bb6933..c90a7222ba10b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -35,7 +35,8 @@ export const createExternalService: ServiceFactory = ( configurationUtilities: ActionsConfigurationUtilities, { table, importSetTable, useImportAPI, appScope }: SNProductsConfigValue ): ExternalService => { - const { apiUrl: url, isLegacy } = config as ServiceNowPublicConfigurationType; + const { apiUrl: url, usesTableApi: usesTableApiConfigValue } = + config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; if (!url || !username || !password) { @@ -57,11 +58,11 @@ export const createExternalService: ServiceFactory = ( auth: { username, password }, }); - const useOldApi = !useImportAPI || isLegacy; + const useTableApi = !useImportAPI || usesTableApiConfigValue; - const getCreateIncidentUrl = () => (useOldApi ? tableApiIncidentUrl : importSetTableUrl); + const getCreateIncidentUrl = () => (useTableApi ? tableApiIncidentUrl : importSetTableUrl); const getUpdateIncidentUrl = (incidentId: string) => - useOldApi ? `${tableApiIncidentUrl}/${incidentId}` : importSetTableUrl; + useTableApi ? `${tableApiIncidentUrl}/${incidentId}` : importSetTableUrl; const getIncidentViewURL = (id: string) => { // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html @@ -105,7 +106,7 @@ export const createExternalService: ServiceFactory = ( /** * Gets the Elastic SN Application information including the current version. - * It should not be used on legacy connectors. + * It should not be used on connectors that use the old API. */ const getApplicationInformation = async (): Promise => { try { @@ -129,7 +130,7 @@ export const createExternalService: ServiceFactory = ( logger.debug(`Create incident: Application scope: ${scope}: Application version${version}`); const checkIfApplicationIsInstalled = async () => { - if (!useOldApi) { + if (!useTableApi) { const { version, scope } = await getApplicationInformation(); logApplicationInfo(scope, version); } @@ -180,17 +181,17 @@ export const createExternalService: ServiceFactory = ( url: getCreateIncidentUrl(), logger, method: 'post', - data: prepareIncident(useOldApi, incident), + data: prepareIncident(useTableApi, incident), configurationUtilities, }); checkInstance(res); - if (!useOldApi) { + if (!useTableApi) { throwIfImportSetApiResponseIsAnError(res.data); } - const incidentId = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id; + const incidentId = useTableApi ? res.data.result.sys_id : res.data.result[0].sys_id; const insertedIncident = await getIncident(incidentId); return { @@ -212,23 +213,23 @@ export const createExternalService: ServiceFactory = ( axios: axiosInstance, url: getUpdateIncidentUrl(incidentId), // Import Set API supports only POST. - method: useOldApi ? 'patch' : 'post', + method: useTableApi ? 'patch' : 'post', logger, data: { - ...prepareIncident(useOldApi, incident), + ...prepareIncident(useTableApi, incident), // elastic_incident_id is used to update the incident when using the Import Set API. - ...(useOldApi ? {} : { elastic_incident_id: incidentId }), + ...(useTableApi ? {} : { elastic_incident_id: incidentId }), }, configurationUtilities, }); checkInstance(res); - if (!useOldApi) { + if (!useTableApi) { throwIfImportSetApiResponseIsAnError(res.data); } - const id = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id; + const id = useTableApi ? res.data.result.sys_id : res.data.result[0].sys_id; const updatedIncident = await getIncident(id); return { diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts index 9f8e62c77e3a7..6c61d9849c72c 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts @@ -166,7 +166,7 @@ describe('successful migrations', () => { expect(migratedAction).toEqual(action); }); - test('set isLegacy config property for .servicenow', () => { + test('set usesTableApi config property for .servicenow', () => { const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; const action = getMockDataForServiceNow(); const migratedAction = migration716(action, context); @@ -177,13 +177,13 @@ describe('successful migrations', () => { ...action.attributes, config: { apiUrl: 'https://example.com', - isLegacy: true, + usesTableApi: true, }, }, }); }); - test('set isLegacy config property for .servicenow-sir', () => { + test('set usesTableApi config property for .servicenow-sir', () => { const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; const action = getMockDataForServiceNow({ actionTypeId: '.servicenow-sir' }); const migratedAction = migration716(action, context); @@ -194,13 +194,13 @@ describe('successful migrations', () => { ...action.attributes, config: { apiUrl: 'https://example.com', - isLegacy: true, + usesTableApi: true, }, }, }); }); - test('it does not set isLegacy config for other connectors', () => { + test('it does not set usesTableApi config for other connectors', () => { const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; const action = getMockData(); const migratedAction = migration716(action, context); diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts index 688839eb89858..2e5b1b5d916fe 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts @@ -68,7 +68,7 @@ export function getActionsMigrations( doc.attributes.actionTypeId === '.servicenow' || doc.attributes.actionTypeId === '.servicenow-sir' || doc.attributes.actionTypeId === '.email', - pipeMigrations(markOldServiceNowITSMConnectorAsLegacy, setServiceConfigIfNotSet) + pipeMigrations(addUsesTableApiToServiceNowConnectors, setServiceConfigIfNotSet) ); const migrationActions800 = createEsoMigration( @@ -197,7 +197,7 @@ const addIsMissingSecretsField = ( }; }; -const markOldServiceNowITSMConnectorAsLegacy = ( +const addUsesTableApiToServiceNowConnectors = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc => { if ( @@ -213,7 +213,7 @@ const markOldServiceNowITSMConnectorAsLegacy = ( ...doc.attributes, config: { ...doc.attributes.config, - isLegacy: true, + usesTableApi: true, }, }, }; diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index 4e0f1689bd4d1..9bbddfae2f9bd 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -123,11 +123,11 @@ describe('Connectors', () => { ).toBe('Update My Connector'); }); - test('it shows the deprecated callout when the connector is legacy', async () => { + test('it shows the deprecated callout when the connector is deprecated', async () => { render( , { // wrapper: TestProviders produces a TS error diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index 1b575e3ba9334..b7bf7c322f76e 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -23,7 +23,7 @@ import { ActionConnector, CaseConnectorMapping } from '../../containers/configur import { Mapping } from './mapping'; import { ActionTypeConnector, ConnectorTypes } from '../../../common'; import { DeprecatedCallout } from '../connectors/deprecated_callout'; -import { isLegacyConnector } from '../utils'; +import { isDeprecatedConnector } from '../utils'; const EuiFormRowExtended = styled(EuiFormRow)` .euiFormRow__labelWrapper { @@ -111,7 +111,7 @@ const ConnectorsComponent: React.FC = ({ appendAddConnectorButton={true} /> - {selectedConnector.type !== ConnectorTypes.none && isLegacyConnector(connector) && ( + {selectedConnector.type !== ConnectorTypes.none && isDeprecatedConnector(connector) && ( diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 6f05f9f940d25..03ed3d6512638 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -171,7 +171,7 @@ describe('ConnectorsDropdown', () => { "value": "servicenow-sir", }, Object { - "data-test-subj": "dropdown-connector-servicenow-legacy", + "data-test-subj": "dropdown-connector-servicenow-uses-table-api", "inputDisplay": { /> , - "value": "servicenow-legacy", + "value": "servicenow-uses-table-api", }, ] `); @@ -288,8 +288,8 @@ describe('ConnectorsDropdown', () => { ).not.toThrowError(); }); - test('it shows the deprecated tooltip when the connector is legacy', () => { - render(, { + test('it shows the deprecated tooltip when the connector is deprecated', () => { + render(, { wrapper: ({ children }) => {children}, }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index c5fe9c7470745..c7ce3c5b3c4b6 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -13,7 +13,7 @@ import { ConnectorTypes } from '../../../common'; import { ActionConnector } from '../../containers/configure/types'; import * as i18n from './translations'; import { useKibana } from '../../common/lib/kibana'; -import { getConnectorIcon, isLegacyConnector } from '../utils'; +import { getConnectorIcon, isDeprecatedConnector } from '../utils'; import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; export interface Props { @@ -95,10 +95,10 @@ const ConnectorsDropdownComponent: React.FC = ({ {connector.name} - {isLegacyConnector(connector) && ` (${i18n.DEPRECATED_TOOLTIP_TEXT})`} + {isDeprecatedConnector(connector) && ` (${i18n.DEPRECATED_TOOLTIP_TEXT})`} - {isLegacyConnector(connector) && ( + {isDeprecatedConnector(connector) && ( { render(); expect(screen.getByText('This connector type is deprecated')).toBeInTheDocument(); expect(screen.getByText('Update this connector, or create a new one.')).toBeInTheDocument(); - expect(screen.getByTestId('legacy-connector-warning-callout')).toHaveClass( + expect(screen.getByTestId('deprecated-connector-warning-callout')).toHaveClass( 'euiCallOut euiCallOut--warning' ); }); test('it renders a danger flyout correctly', () => { render(); - expect(screen.getByTestId('legacy-connector-warning-callout')).toHaveClass( + expect(screen.getByTestId('deprecated-connector-warning-callout')).toHaveClass( 'euiCallOut euiCallOut--danger' ); }); diff --git a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx index 9337f2843506b..195b2deb84d6e 100644 --- a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx +++ b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx @@ -9,15 +9,15 @@ import React from 'react'; import { EuiCallOut, EuiCallOutProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -const LEGACY_CONNECTOR_WARNING_TITLE = i18n.translate( - 'xpack.cases.connectors.serviceNow.legacyConnectorWarningTitle', +const DEPRECATED_CONNECTOR_WARNING_TITLE = i18n.translate( + 'xpack.cases.connectors.serviceNow.deprecatedConnectorWarningTitle', { defaultMessage: 'This connector type is deprecated', } ); -const LEGACY_CONNECTOR_WARNING_DESC = i18n.translate( - 'xpack.cases.connectors.serviceNow.legacyConnectorWarningDesc', +const DEPRECATED_CONNECTOR_WARNING_DESC = i18n.translate( + 'xpack.cases.connectors.serviceNow.deprecatedConnectorWarningDesc', { defaultMessage: 'Update this connector, or create a new one.', } @@ -29,12 +29,12 @@ interface Props { const DeprecatedCalloutComponent: React.FC = ({ type = 'warning' }) => ( - {LEGACY_CONNECTOR_WARNING_DESC} + {DEPRECATED_CONNECTOR_WARNING_DESC} ); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index 008340b6b7e97..324dcef8ba397 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -127,15 +127,15 @@ describe('ServiceNowITSM Fields', () => { ); }); - test('it shows the deprecated callout when the connector is legacy', async () => { - const legacyConnector = { ...connector, config: { isLegacy: true } }; - render(); - expect(screen.getByTestId('legacy-connector-warning-callout')).toBeInTheDocument(); + test('it shows the deprecated callout when the connector uses the table API', async () => { + const tableApiConnector = { ...connector, config: { usesTableApi: true } }; + render(); + expect(screen.getByTestId('deprecated-connector-warning-callout')).toBeInTheDocument(); }); - test('it does not show the deprecated callout when the connector is not legacy', async () => { + test('it does not show the deprecated callout when the connector does not uses the table API', async () => { render(); - expect(screen.queryByTestId('legacy-connector-warning-callout')).not.toBeInTheDocument(); + expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); }); describe('onChange calls', () => { diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx index aac78b8266fb5..cd8f5f4abf7b5 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -161,15 +161,15 @@ describe('ServiceNowSIR Fields', () => { ]); }); - test('it shows the deprecated callout when the connector is legacy', async () => { - const legacyConnector = { ...connector, config: { isLegacy: true } }; - render(); - expect(screen.getByTestId('legacy-connector-warning-callout')).toBeInTheDocument(); + test('it shows the deprecated callout when the connector uses the table API', async () => { + const tableApiConnector = { ...connector, config: { usesTableApi: true } }; + render(); + expect(screen.getByTestId('deprecated-connector-warning-callout')).toBeInTheDocument(); }); - test('it does not show the deprecated callout when the connector is not legacy', async () => { + test('it does not show the deprecated callout when the connector does not uses the table API', async () => { render(); - expect(screen.queryByTestId('legacy-connector-warning-callout')).not.toBeInTheDocument(); + expect(screen.queryByTestId('deprecated-connector-warning-callout')).not.toBeInTheDocument(); }); describe('onChange calls', () => { diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts index c098d803276bc..aa643191ac62e 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts @@ -10,24 +10,24 @@ import { connectorValidator } from './validator'; describe('ServiceNow validator', () => { describe('connectorValidator', () => { - test('it returns an error message if the connector is legacy', () => { + test('it returns an error message if the connector uses the table API', () => { const invalidConnector = { ...connector, config: { ...connector.config, - isLegacy: true, + usesTableApi: true, }, }; expect(connectorValidator(invalidConnector)).toEqual({ message: 'Deprecated connector' }); }); - test('it does not returns an error message if the connector is not legacy', () => { + test('it does not returns an error message if the connector does not uses the table API', () => { const invalidConnector = { ...connector, config: { ...connector.config, - isLegacy: false, + usesTableApi: false, }, }; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts index 3f67f25549343..7d56163c48350 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts @@ -9,16 +9,16 @@ import { ValidationConfig } from '../../../common/shared_imports'; import { CaseActionConnector } from '../../types'; /** - * The user can not use a legacy connector + * The user can not create cases with connectors that use the table API */ export const connectorValidator = ( connector: CaseActionConnector ): ReturnType => { const { - config: { isLegacy }, + config: { usesTableApi }, } = connector; - if (isLegacy) { + if (usesTableApi) { return { message: 'Deprecated connector', }; diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index ac5f4dbdd298e..74137789958a4 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -78,7 +78,7 @@ export const getConnectorIcon = ( }; // TODO: Remove when the applications are certified -export const isLegacyConnector = (connector?: CaseActionConnector) => { +export const isDeprecatedConnector = (connector?: CaseActionConnector): boolean => { if (connector == null) { return true; } @@ -91,5 +91,16 @@ export const isLegacyConnector = (connector?: CaseActionConnector) => { return true; } - return connector.config.isLegacy; + /** + * Connector's prior to the Elastic ServiceNow application + * use the Table API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_TableAPI) + * Connectors after the Elastic ServiceNow application use the + * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) + * A ServiceNow connector is considered deprecated if it uses the Table API. + * + * All other connectors do not have the usesTableApi config property + * so the function will always return false for them. + */ + + return !!connector.config.usesTableApi; }; diff --git a/x-pack/plugins/cases/public/containers/configure/mock.ts b/x-pack/plugins/cases/public/containers/configure/mock.ts index d1ae7f310a719..a5483e524e92d 100644 --- a/x-pack/plugins/cases/public/containers/configure/mock.ts +++ b/x-pack/plugins/cases/public/containers/configure/mock.ts @@ -72,12 +72,12 @@ export const connectorsMock: ActionConnector[] = [ isPreconfigured: false, }, { - id: 'servicenow-legacy', + id: 'servicenow-uses-table-api', actionTypeId: '.servicenow', name: 'My Connector', config: { apiUrl: 'https://instance1.service-now.com', - isLegacy: true, + usesTableApi: true, }, isPreconfigured: false, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts index e6acb2e0976a8..755923acc25cb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts @@ -28,7 +28,7 @@ export const isFieldInvalid = ( ): boolean => error !== undefined && error.length > 0 && field != null; // TODO: Remove when the applications are certified -export const isLegacyConnector = (connector: ServiceNowActionConnector) => { +export const isDeprecatedConnector = (connector: ServiceNowActionConnector): boolean => { if (connector == null) { return true; } @@ -41,5 +41,14 @@ export const isLegacyConnector = (connector: ServiceNowActionConnector) => { return true; } - return connector.config.isLegacy; + /** + * Connectors after the Elastic ServiceNow application use the + * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) + * A ServiceNow connector is considered deprecated if it uses the Table API. + * + * All other connectors do not have the usesTableApi config property + * so the function will always return false for them. + */ + + return !!connector.config.usesTableApi; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index eb3e1c01887c9..3f22a51b5bd53 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -52,7 +52,7 @@ describe('servicenow connector validation', () => { isPreconfigured: false, config: { apiUrl: 'https://dev94428.service-now.com/', - isLegacy: false, + usesTableApi: false, }, } as ServiceNowActionConnector; @@ -60,7 +60,7 @@ describe('servicenow connector validation', () => { config: { errors: { apiUrl: [], - isLegacy: [], + usesTableApi: [], }, }, secrets: { @@ -88,7 +88,7 @@ describe('servicenow connector validation', () => { config: { errors: { apiUrl: ['URL is required.'], - isLegacy: [], + usesTableApi: [], }, }, secrets: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 6b6d536ff303b..7267e11ae7327 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -28,7 +28,7 @@ const validateConnector = async ( const translations = await import('./translations'); const configErrors = { apiUrl: new Array(), - isLegacy: new Array(), + usesTableApi: new Array(), }; const secretsErrors = { username: new Array(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index f491376e5078c..03acb673bf5a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -36,7 +36,7 @@ describe('ServiceNowActionConnectorFields renders', () => { name: 'SN', config: { apiUrl: 'https://test/', - isLegacy: true, + usesTableApi: true, }, } as ServiceNowActionConnector; @@ -44,7 +44,7 @@ describe('ServiceNowActionConnectorFields renders', () => { ...usesTableApiConnector, config: { ...usesTableApiConnector.config, - isLegacy: false, + usesTableApi: false, }, } as ServiceNowActionConnector; @@ -350,7 +350,7 @@ describe('ServiceNowActionConnectorFields renders', () => { id: usesTableApiConnector.id, connector: { name: usesTableApiConnector.name, - config: { ...usesTableApiConnector.config, isLegacy: false }, + config: { ...usesTableApiConnector.config, usesTableApi: false }, secrets: usesTableApiConnector.secrets, }, }) @@ -415,7 +415,7 @@ describe('ServiceNowActionConnectorFields renders', () => { ).toBeTruthy(); }); - test('should set the isLegacy to false when creating a connector', async () => { + test('should set the usesTableApi to false when creating a connector', async () => { const newConnector = { ...usesTableApiConnector, config: {}, secrets: {} }; const editActionConfig = jest.fn(); @@ -432,7 +432,7 @@ describe('ServiceNowActionConnectorFields renders', () => { /> ); - expect(editActionConfig).toHaveBeenCalledWith('isLegacy', false); + expect(editActionConfig).toHaveBeenCalledWith('usesTableApi', false); }); test('it should set the legacy attribute if it is not undefined', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index a0b4bdca47ff5..db3c32755f0ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -15,7 +15,7 @@ import { useKibana } from '../../../../common/lib/kibana'; import { DeprecatedCallout } from './deprecated_callout'; import { useGetAppInfo } from './use_get_app_info'; import { ApplicationRequiredCallout } from './application_required_callout'; -import { isRESTApiError, isLegacyConnector } from './helpers'; +import { isRESTApiError, isDeprecatedConnector } from './helpers'; import { InstallationCallout } from './installation_callout'; import { UpdateConnector } from './update_connector'; import { updateActionConnector } from '../../../lib/action_connector_api'; @@ -36,9 +36,9 @@ const ServiceNowConnectorFields: React.FC(false); + const [showApplicationRequiredCallout, setShowApplicationRequiredCallout] = + useState(false); const [applicationInfoErrorMsg, setApplicationInfoErrorMsg] = useState(null); const getApplicationInfo = useCallback(async () => { - setApplicationRequired(false); + setShowApplicationRequiredCallout(false); setApplicationInfoErrorMsg(null); try { @@ -61,7 +62,7 @@ const ServiceNowConnectorFields: React.FC { - if (!isOldConnector) { + if (requiresNewApplication) { await getApplicationInfo(); } - }, [getApplicationInfo, isOldConnector]); + }, [getApplicationInfo, requiresNewApplication]); useEffect( () => setCallbacks({ beforeActionConnectorSave }), @@ -90,13 +91,13 @@ const ServiceNowConnectorFields: React.FC { - if (isLegacy == null) { - editActionConfig('isLegacy', false); + if (usesTableApi == null) { + editActionConfig('usesTableApi', false); } }); @@ -150,8 +151,8 @@ const ServiceNowConnectorFields: React.FC )} - {!isOldConnector && } - {isOldConnector && } + {requiresNewApplication && } + {!requiresNewApplication && } - {applicationRequired && !isOldConnector && ( + {showApplicationRequiredCallout && requiresNewApplication && ( )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index 09b04f0fa3c48..dcfdfe3af0e0e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -23,7 +23,7 @@ import { ServiceNowITSMActionParams, Choice, Fields, ServiceNowActionConnector } import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { useGetChoices } from './use_get_choices'; -import { choicesToEuiOptions, DEFAULT_CORRELATION_ID, isLegacyConnector } from './helpers'; +import { choicesToEuiOptions, DEFAULT_CORRELATION_ID, isDeprecatedConnector } from './helpers'; import * as i18n from './translations'; @@ -46,10 +46,6 @@ const ServiceNowParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; - const isDeprecatedConnector = isLegacyConnector( - actionConnector as unknown as ServiceNowActionConnector - ); - const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( () => @@ -244,7 +240,7 @@ const ServiceNowParamsFields: React.FunctionComponent< - {!isDeprecatedConnector && ( + {!isDeprecatedConnector(actionConnector as unknown as ServiceNowActionConnector) && ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx index 42758250408d9..a264ed5421c2e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx @@ -25,7 +25,7 @@ import { TextFieldWithMessageVariables } from '../../text_field_with_message_var import * as i18n from './translations'; import { useGetChoices } from './use_get_choices'; import { ServiceNowSIRActionParams, Fields, Choice, ServiceNowActionConnector } from './types'; -import { choicesToEuiOptions, isLegacyConnector, DEFAULT_CORRELATION_ID } from './helpers'; +import { choicesToEuiOptions, isDeprecatedConnector, DEFAULT_CORRELATION_ID } from './helpers'; const useGetChoicesFields = ['category', 'subcategory', 'priority']; const defaultFields: Fields = { @@ -43,10 +43,6 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; - const isDeprecatedConnector = isLegacyConnector( - actionConnector as unknown as ServiceNowActionConnector - ); - const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( () => @@ -229,7 +225,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< - {!isDeprecatedConnector && ( + {!isDeprecatedConnector(actionConnector as unknown as ServiceNowActionConnector) && ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts index ecbb4f9de726b..40bb47543a3c8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts @@ -35,7 +35,7 @@ export interface ServiceNowITOMActionParams { export interface ServiceNowConfig { apiUrl: string; - isLegacy: boolean; + usesTableApi: boolean; } export interface ServiceNowSecrets { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx index 2d95bfa85ceb9..ecb90051e78c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx @@ -22,7 +22,7 @@ const actionConnector: ServiceNowActionConnector = { name: 'servicenow', config: { apiUrl: 'https://test/', - isLegacy: true, + usesTableApi: true, }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx index c6b70443ec8fb..f842f6863676a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx @@ -34,7 +34,7 @@ const actionConnector = { isPreconfigured: false, config: { apiUrl: 'https://test.service-now.com/', - isLegacy: false, + usesTableApi: false, }, } as ServiceNowActionConnector; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 844f28f022547..5de21470fc19a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -205,8 +205,8 @@ const ActionsConnectorsList: React.FunctionComponent = () => { const itemConfig = ( item as UserConfiguredActionConnector, Record> ).config; - const showLegacyTooltip = - itemConfig?.isLegacy && + const showDeprecatedTooltip = + itemConfig?.usesTableApi && // TODO: Remove when applications are certified ((ENABLE_NEW_SN_ITSM_CONNECTOR && item.actionTypeId === '.servicenow') || (ENABLE_NEW_SN_SIR_CONNECTOR && item.actionTypeId === '.servicenow-sir')); @@ -233,7 +233,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { position="right" /> ) : null} - {showLegacyTooltip && } + {showDeprecatedTooltip && } ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts index 97c2a77a8f074..1308959ebbacf 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts @@ -22,7 +22,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - isLegacy: false, + usesTableApi: false, }, secrets: { password: 'elastic', @@ -91,7 +91,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow', config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, secrets: mockServiceNow.secrets, }) @@ -105,7 +105,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { is_missing_secrets: false, config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, }); @@ -121,12 +121,12 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { is_missing_secrets: false, config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, }); }); - it('should set the isLegacy to true when not provided', async () => { + it('should set the usesTableApi to true when not provided', async () => { const { body: createdAction } = await supertest .post('/api/actions/connector') .set('kbn-xsrf', 'foo') @@ -144,7 +144,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { .get(`/api/actions/connector/${createdAction.id}`) .expect(200); - expect(fetchedAction.config.isLegacy).to.be(true); + expect(fetchedAction.config.usesTableApi).to.be(true); }); it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { @@ -223,7 +223,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow', config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, secrets: mockServiceNow.secrets, }); @@ -383,7 +383,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { }); describe('Execution', () => { - // New connectors + // Connectors that use the Import set API describe('Import set API', () => { it('should handle creating an incident without comments', async () => { const { body: result } = await supertest @@ -414,7 +414,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { }); }); - // Legacy connectors + // Connectors that use the Table API describe('Table API', () => { before(async () => { const { body } = await supertest @@ -425,7 +425,7 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow', config: { apiUrl: serviceNowSimulatorURL, - isLegacy: true, + usesTableApi: true, }, secrets: mockServiceNow.secrets, }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts index 3a13e055e7aeb..c27634ecf6aca 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts @@ -22,7 +22,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - isLegacy: false, + usesTableApi: false, }, secrets: { password: 'elastic', @@ -95,7 +95,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow-sir', config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, secrets: mockServiceNow.secrets, }) @@ -109,7 +109,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { is_missing_secrets: false, config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, }); @@ -125,12 +125,12 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { is_missing_secrets: false, config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, }); }); - it('should set the isLegacy to true when not provided', async () => { + it('should set the usesTableApi to true when not provided', async () => { const { body: createdAction } = await supertest .post('/api/actions/connector') .set('kbn-xsrf', 'foo') @@ -148,7 +148,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { .get(`/api/actions/connector/${createdAction.id}`) .expect(200); - expect(fetchedAction.config.isLegacy).to.be(true); + expect(fetchedAction.config.usesTableApi).to.be(true); }); it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { @@ -227,7 +227,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow-sir', config: { apiUrl: serviceNowSimulatorURL, - isLegacy: false, + usesTableApi: false, }, secrets: mockServiceNow.secrets, }); @@ -387,7 +387,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { }); describe('Execution', () => { - // New connectors + // Connectors that use the Import set API describe('Import set API', () => { it('should handle creating an incident without comments', async () => { const { body: result } = await supertest @@ -418,7 +418,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { }); }); - // Legacy connectors + // Connectors that use the Table API describe('Table API', () => { before(async () => { const { body } = await supertest @@ -429,7 +429,7 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow-sir', config: { apiUrl: serviceNowSimulatorURL, - isLegacy: true, + usesTableApi: true, }, secrets: mockServiceNow.secrets, }); diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 0a875f9f1e822..9d48aed32d55c 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -329,7 +329,7 @@ export const getServiceNowConnector = () => ({ }, config: { apiUrl: 'http://some.non.existent.com', - isLegacy: false, + usesTableApi: false, }, }); @@ -386,7 +386,7 @@ export const getServiceNowSIRConnector = () => ({ }, config: { apiUrl: 'http://some.non.existent.com', - isLegacy: false, + usesTableApi: false, }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index c3e737464f19b..a4e69ab928325 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -109,7 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com', - isLegacy: false, + usesTableApi: false, }, isPreconfigured: false, isMissingSecrets: false, @@ -121,7 +121,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com', - isLegacy: false, + usesTableApi: false, }, isPreconfigured: false, isMissingSecrets: false, diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 7b6848d1f301e..02b91c9f0b918 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -109,7 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com', - isLegacy: false, + usesTableApi: false, }, isPreconfigured: false, isMissingSecrets: false, @@ -121,7 +121,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com', - isLegacy: false, + usesTableApi: false, }, isPreconfigured: false, isMissingSecrets: false, From 504896d51fd50c709686a398e24f1b2ae02dab98 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 25 Oct 2021 19:19:46 +0200 Subject: [PATCH 41/41] [Fleet] Fix showing deployment details callout on Cloud staging (#116123) --- .../integrations/components/header/deployment_details.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx index 48c8fa56fb91b..79175d241c29f 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx @@ -32,7 +32,8 @@ export const DeploymentDetails = () => { !( cname.endsWith('elastic-cloud.com') || cname.endsWith('found.io') || - cname.endsWith('found.no') + cname.endsWith('found.no') || + cname.endsWith('foundit.no') ) ) { return null;