From 8038127a0e453c99081e431657aa3487a08df9cc Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 29 Nov 2022 09:23:32 -0600 Subject: [PATCH 01/11] Add jest to renovate config (#145877) This adds a renovate config for core jest packages, but skips a few jest related libraries. They need to be updated separately and then added to the config. jest-styled-components breaks a bunch of snapshots, for example. --- renovate.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/renovate.json b/renovate.json index 9c0f4150b9c80..d4a31a44a40b0 100644 --- a/renovate.json +++ b/renovate.json @@ -158,6 +158,30 @@ "labels": ["Team:Operations", "release_note:skip"], "enabled": true }, + { + "groupName": "jest", + "packageNames": [ + "@jest/console", + "@jest/reporters", + "@jest/types", + "@types/jest", + "babel-jest", + "expect", + "jest", + "jest-cli", + "jest-config", + "jest-diff", + "jest-environment-jsdom", + "jest-matcher-utils", + "jest-mock", + "jest-runtime", + "jest-snapshot" + ], + "reviewers": ["team:kibana-operations"], + "matchBaseBranches": ["main"], + "labels": ["Team:Operations", "release_note:skip"], + "enabled": true + }, { "groupName": "@storybook", "reviewers": ["team:kibana-operations"], From 3412b3e9348fe07463eb411a0e701640bec3ccff Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Tue, 29 Nov 2022 07:26:02 -0800 Subject: [PATCH 02/11] [Security Solution][Alerts] Include docs that don't populate the groupBy field for suppression (#145784) ## Summary Updates custom query rule alert suppression logic to group all documents that don't populate a groupBy field together instead of excluding them from the results entirely. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../components/alerts_table/actions.tsx | 38 +++++++++++++------ .../rules/step_define_rule/index.tsx | 1 + ...ld_group_by_field_aggregation.test.ts.snap | 1 + .../build_group_by_field_aggregation.ts | 1 + .../rule_execution_logic/query.ts | 19 +++++++++- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 804289c9cdc7a..d1ce1901ca19d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -722,18 +722,32 @@ const getSuppressedAlertData = (ecsData: Ecs | Ecs[]) => { ); const dataProviderPartials = terms.map((term) => { const fieldId = term.field.replace('.', '-'); - return { - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${fieldId}-${term.value}`, - name: fieldId, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: term.field, - value: term.value, - operator: ':' as const, - }, - }; + const id = `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${fieldId}-${term.value}`; + return term.value == null + ? { + id, + name: fieldId, + enabled: true, + excluded: true, + kqlQuery: '', + queryMatch: { + field: term.field, + value: '', + operator: ':*' as const, + }, + } + : { + id, + name: fieldId, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: term.field, + value: term.value, + operator: ':' as const, + }, + }; }); const dataProvider = { ...dataProviderPartials[0], diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index bdf7fb219b859..45db51db150f6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -167,6 +167,7 @@ const StepDefineRuleComponent: FC = ({ 'newTermsFields', 'historyWindowSize', 'shouldLoadQueryDynamically', + 'groupByFields', ], onChange: (data: DefineStepRule) => { if (onRuleDataChange) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/__snapshots__/build_group_by_field_aggregation.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/__snapshots__/build_group_by_field_aggregation.test.ts.snap index c1b21b1de1db1..f1f3e409217f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/__snapshots__/build_group_by_field_aggregation.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/__snapshots__/build_group_by_field_aggregation.test.ts.snap @@ -35,6 +35,7 @@ Object { "host.name": Object { "terms": Object { "field": "host.name", + "missing_bucket": true, }, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/build_group_by_field_aggregation.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/build_group_by_field_aggregation.ts index 4df370d6bced9..88b2c4f450862 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/build_group_by_field_aggregation.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/alert_suppression/build_group_by_field_aggregation.ts @@ -22,6 +22,7 @@ export const buildGroupByFieldAggregation = ({ [field]: { terms: { field, + missing_bucket: true, }, }, })), diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts index 8ef0bf6b736dd..074989e424cfe 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts @@ -623,7 +623,7 @@ export default ({ getService }: FtrProviderContext) => { it('should generate multiple alerts for a single doc in multiple groups', async () => { const rule: QueryRuleCreateProps = { ...getRuleForSignalTesting(['suppression-data']), - query: `destination.ip: *`, + query: `*:*`, alert_suppression: { group_by: ['destination.ip'], }, @@ -642,7 +642,7 @@ export default ({ getService }: FtrProviderContext) => { size: 1000, sort: ['destination.ip'], }); - expect(previewAlerts.length).to.eql(2); + expect(previewAlerts.length).to.eql(3); expect(previewAlerts[0]._source).to.eql({ ...previewAlerts[0]._source, @@ -657,6 +657,21 @@ export default ({ getService }: FtrProviderContext) => { [ALERT_SUPPRESSION_END]: '2020-10-28T05:00:00.000Z', [ALERT_SUPPRESSION_DOCS_COUNT]: 0, }); + + // We also expect to have a separate group for documents that don't populate the groupBy field + expect(previewAlerts[2]._source).to.eql({ + ...previewAlerts[2]._source, + [ALERT_SUPPRESSION_TERMS]: [ + { + field: 'destination.ip', + value: null, + }, + ], + [ALERT_ORIGINAL_TIME]: '2020-10-28T05:00:00.000Z', + [ALERT_SUPPRESSION_START]: '2020-10-28T05:00:00.000Z', + [ALERT_SUPPRESSION_END]: '2020-10-28T05:00:02.000Z', + [ALERT_SUPPRESSION_DOCS_COUNT]: 16, + }); }); }); }); From d86313f2c5b6bb104d7ded8b344f09517a644726 Mon Sep 17 00:00:00 2001 From: Yngrid Coello Date: Tue, 29 Nov 2022 17:02:57 +0100 Subject: [PATCH 03/11] [APM] Added env filter to agent explorer ui (#146326) Relates to https://github.com/elastic/kibana/issues/146019. This PR added environment filter to Agent explorer view. https://user-images.githubusercontent.com/1313018/203838057-0892c0e1-4023-400e-8217-b394002860df.mov Since Agent explorer doesn't have a timeRange component, takes into account always data from the last 24h, I added a `customTimeRange` optional input to `EnvironmentsContextProvider`. Apart from this the `serviceName`, `rangeFrom` and `rangeTo` that we use in `EnvironmentSelect` are coming now from the context as well instead of picking them from the url. --- .../app/settings/agent_explorer/index.tsx | 14 +++++++++++++- .../shared/environment_filter/index.tsx | 10 +++++++++- .../shared/environment_select/index.tsx | 11 ++++++----- .../environments_context.tsx | 17 ++++++++++++++--- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_explorer/index.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_explorer/index.tsx index 27e7c14706cd3..477a8df81dc26 100644 --- a/x-pack/plugins/apm/public/components/app/settings/agent_explorer/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/agent_explorer/index.tsx @@ -21,10 +21,12 @@ import { SERVICE_LANGUAGE_NAME, SERVICE_NAME, } from '../../../../../common/es_fields/apm'; +import { EnvironmentsContextProvider } from '../../../../context/environments_context/environments_context'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher'; import { useTimeRange } from '../../../../hooks/use_time_range'; +import { ApmEnvironmentFilter } from '../../../shared/environment_filter'; import { KueryBar } from '../../../shared/kuery_bar'; import * as urlHelpers from '../../../shared/links/url_helpers'; import { SuggestionsSelect } from '../../../shared/suggestions_select'; @@ -68,7 +70,10 @@ export function AgentExplorer() { query: { serviceName, agentLanguage }, } = useApmParams('/settings/agent-explorer'); - const { start, end } = useTimeRange({ rangeFrom: 'now-24h', rangeTo: 'now' }); + const rangeFrom = 'now-24h'; + const rangeTo = 'now'; + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const agents = useAgentExplorerFetcher({ start, end }); const isLoading = agents.status === FETCH_STATUS.LOADING; @@ -120,6 +125,13 @@ export function AgentExplorer() { + + + + + updateEnvironmentUrl(history, location, changeValue) } diff --git a/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx b/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx index a6b14b4f0a4e8..3ddc57a78f56d 100644 --- a/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx @@ -17,7 +17,6 @@ import { import { SERVICE_ENVIRONMENT } from '../../../../common/es_fields/apm'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; -import { useApmParams } from '../../../hooks/use_apm_params'; import { Environment } from '../../../../common/environment_rt'; function getEnvironmentOptions(environments: Environment[]) { @@ -41,18 +40,20 @@ export function EnvironmentSelect({ environment, availableEnvironments, status, + serviceName, + rangeFrom, + rangeTo, onChange, }: { environment: Environment; availableEnvironments: Environment[]; status: FETCH_STATUS; + serviceName?: string; + rangeFrom: string; + rangeTo: string; onChange: (value: string) => void; }) { const [searchValue, setSearchValue] = useState(''); - const { - path: { serviceName }, - query: { rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}/*'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx b/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx index 23e5a79084526..a065d82bbc1ac 100644 --- a/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx +++ b/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx @@ -8,15 +8,18 @@ import React from 'react'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { Environment } from '../../../common/environment_rt'; import { useApmParams } from '../../hooks/use_apm_params'; +import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher'; import { FETCH_STATUS } from '../../hooks/use_fetcher'; import { useTimeRange } from '../../hooks/use_time_range'; -import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher'; export const EnvironmentsContext = React.createContext<{ environment: Environment; environments: Environment[]; status: FETCH_STATUS; preferredEnvironment: Environment; + serviceName?: string; + rangeFrom?: string; + rangeTo?: string; }>({ environment: ENVIRONMENT_ALL.value, environments: [], @@ -26,8 +29,10 @@ export const EnvironmentsContext = React.createContext<{ export function EnvironmentsContextProvider({ children, + customTimeRange, }: { children: React.ReactElement; + customTimeRange?: { rangeFrom: string; rangeTo: string }; }) { const { path, query } = useApmParams('/*'); @@ -36,8 +41,11 @@ export function EnvironmentsContextProvider({ ('environment' in query && (query.environment as Environment)) || ENVIRONMENT_ALL.value; - const rangeFrom = 'rangeFrom' in query ? query.rangeFrom : undefined; - const rangeTo = 'rangeTo' in query ? query.rangeTo : undefined; + const queryRangeFrom = 'rangeFrom' in query ? query.rangeFrom : undefined; + const queryRangeTo = 'rangeTo' in query ? query.rangeTo : undefined; + + const rangeFrom = customTimeRange?.rangeFrom || queryRangeFrom; + const rangeTo = customTimeRange?.rangeTo || queryRangeTo; const { start, end } = useTimeRange({ rangeFrom, rangeTo, optional: true }); @@ -58,6 +66,9 @@ export function EnvironmentsContextProvider({ environments, status, preferredEnvironment, + serviceName, + rangeFrom, + rangeTo, }} > {children} From 04360839cdee89e6bf4b457fe2291fa448a00893 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 29 Nov 2022 09:08:54 -0700 Subject: [PATCH 04/11] [maps] Ability to offset point labels on maps (#145773) Resolves https://github.com/elastic/kibana/issues/143677 PR adds "Label position" style setting allowing users to position labels at top, center, or bottom. Screen Shot 2022-11-18 at 12 55 44 PM Screen Shot 2022-11-18 at 12 55 32 PM Screen Shot 2022-11-18 at 12 55 14 PM Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/maps/vector-style-properties.asciidoc | 2 + .../latency_map/get_layer_list.ts | 6 + x-pack/plugins/maps/common/constants.ts | 7 + .../style_property_descriptor_types.ts | 9 + x-pack/plugins/maps/common/index.ts | 1 + .../components/get_vector_style_label.ts | 4 + .../label_position_editor.test.tsx.snap | 122 ++++++ .../label/label_position_editor.test.tsx | 57 +++ .../label/label_position_editor.tsx | 85 +++++ .../vector_style_label_border_size_editor.js | 5 +- .../vector/components/style_prop_editor.tsx | 18 +- .../vector/components/vector_style_editor.tsx | 24 +- .../dynamic_size_property.tsx | 54 +-- .../label_position_property.test.ts | 355 ++++++++++++++++++ .../properties/label_position_property.ts | 168 +++++++++ .../vector/properties/static_icon_property.ts | 9 +- .../classes/styles/vector/vector_style.tsx | 163 ++++---- .../styles/vector/vector_style_defaults.ts | 11 + x-pack/plugins/ux/e2e/journeys/index.ts | 2 +- .../__mocks__/regions_layer.mock.ts | 2 + .../visitor_breakdown_map/use_layer_list.ts | 6 + 21 files changed, 989 insertions(+), 121 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/label/__snapshots__/label_position_editor.test.tsx.snap create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.test.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/label_position_property.test.ts create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/label_position_property.ts diff --git a/docs/maps/vector-style-properties.asciidoc b/docs/maps/vector-style-properties.asciidoc index e702b6d548cd6..431dbac6130e2 100644 --- a/docs/maps/vector-style-properties.asciidoc +++ b/docs/maps/vector-style-properties.asciidoc @@ -14,6 +14,8 @@ You can add text labels to your Point features by configuring label style proper |=== |*Label* |Specifies label content. +|*Label position* +|Place label above, in the center of, or below the Point feature. |*Label visibility* |Specifies the zoom range for which labels are displayed. |*Label color* diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts index 31150b73fcf51..e17d5d4b5663c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts @@ -14,6 +14,7 @@ import { COLOR_MAP_TYPE, FIELD_ORIGIN, LABEL_BORDER_SIZES, + LABEL_POSITIONS, LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, @@ -74,6 +75,11 @@ function getLayerStyle(): VectorStyleDescriptor { }, }, }, + labelPosition: { + options: { + position: LABEL_POSITIONS.CENTER, + }, + }, labelZoomRange: { options: { useLayerZoomRange: true, diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 117bfa0eaaeaf..1085ff18ea428 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -220,6 +220,12 @@ export enum LABEL_BORDER_SIZES { LARGE = 'LARGE', } +export enum LABEL_POSITIONS { + BOTTOM = 'BOTTOM', + CENTER = 'CENTER', + TOP = 'TOP', +} + export const DEFAULT_ICON = 'marker'; export const DEFAULT_CUSTOM_ICON_CUTOFF = 0.25; export const DEFAULT_CUSTOM_ICON_RADIUS = 0.25; @@ -247,6 +253,7 @@ export enum VECTOR_STYLES { LABEL_SIZE = 'labelSize', LABEL_BORDER_COLOR = 'labelBorderColor', LABEL_BORDER_SIZE = 'labelBorderSize', + LABEL_POSITION = 'labelPosition', } export enum SCALING_TYPES { diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index ad04d467741c4..4b9fe7f34c2ef 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -12,6 +12,7 @@ import { FIELD_ORIGIN, ICON_SOURCE, LABEL_BORDER_SIZES, + LABEL_POSITIONS, SYMBOLIZE_AS_TYPES, VECTOR_STYLES, DATA_MAPPING_FUNCTION, @@ -35,6 +36,12 @@ export type LabelBorderSizeStylePropertyDescriptor = { options: LabelBorderSizeOptions; }; +export type LabelPositionStylePropertyDescriptor = { + options: { + position: LABEL_POSITIONS; + }; +}; + export type LabelZoomRangeStylePropertyDescriptor = { options: { useLayerZoomRange: boolean; @@ -216,6 +223,7 @@ export type VectorStylePropertiesDescriptor = { [VECTOR_STYLES.LABEL_SIZE]: SizeStylePropertyDescriptor; [VECTOR_STYLES.LABEL_BORDER_COLOR]: ColorStylePropertyDescriptor; [VECTOR_STYLES.LABEL_BORDER_SIZE]: LabelBorderSizeStylePropertyDescriptor; + [VECTOR_STYLES.LABEL_POSITION]: LabelPositionStylePropertyDescriptor; }; export type StyleDescriptor = { @@ -274,6 +282,7 @@ export type EMSVectorTileStyleDescriptor = StyleDescriptor & { export type StylePropertyOptions = | LabelBorderSizeOptions + | LabelPositionStylePropertyDescriptor['options'] | LabelZoomRangeStylePropertyDescriptor['options'] | SymbolizeAsOptions | DynamicStylePropertyOptions diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index f8a647e9bca35..3fe8b0f7947c3 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -14,6 +14,7 @@ export { FIELD_ORIGIN, INITIAL_LOCATION, LABEL_BORDER_SIZES, + LABEL_POSITIONS, LAYER_TYPE, MAP_SAVED_OBJECT_TYPE, SCALING_TYPES, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.ts b/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.ts index 62c7b0aac2628..9601d6194a7e1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.ts @@ -66,6 +66,10 @@ export function getVectorStyleLabel(styleName: VECTOR_STYLES) { return i18n.translate('xpack.maps.styles.vector.labelBorderWidthLabel', { defaultMessage: 'Label border width', }); + case VECTOR_STYLES.LABEL_POSITION: + return i18n.translate('xpack.maps.styles.vector.labelPositionLabel', { + defaultMessage: 'Label position', + }); default: return styleName; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/label/__snapshots__/label_position_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/label/__snapshots__/label_position_editor.test.tsx.snap new file mode 100644 index 0000000000000..b5c9d2973d29c --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/label/__snapshots__/label_position_editor.test.tsx.snap @@ -0,0 +1,122 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + + + +`; + +exports[`should render as disabled when label is not set 1`] = ` + + + + + +`; + +exports[`should render as disabled when label position is disabled 1`] = ` + + + + + +`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.test.tsx new file mode 100644 index 0000000000000..bd1748f8eb078 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { LABEL_POSITIONS } from '../../../../../../common/constants'; +import { LabelPositionEditor } from './label_position_editor'; +import { LabelPositionProperty } from '../../properties/label_position_property'; + +const defaultProps = { + handlePropertyChange: () => {}, + hasLabel: true, + styleProperty: { + isDisabled: () => { + return false; + }, + getOptions: () => { + return { + position: LABEL_POSITIONS.TOP, + }; + }, + } as unknown as LabelPositionProperty, +}; + +test('should render', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should render as disabled when label is not set', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should render as disabled when label position is disabled', () => { + const disabledLabelPosition = { + isDisabled: () => { + return true; + }, + getOptions: () => { + return { + position: LABEL_POSITIONS.TOP, + }; + }, + getDisabledReason: () => { + return 'simulated disabled error'; + }, + } as unknown as LabelPositionProperty; + const component = shallow( + + ); + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.tsx new file mode 100644 index 0000000000000..8bf84d4cdc4e2 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/label/label_position_editor.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ChangeEvent } from 'react'; + +import { EuiFormRow, EuiSelect, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getVectorStyleLabel, getDisabledByMessage } from '../get_vector_style_label'; +import { LABEL_POSITIONS, VECTOR_STYLES } from '../../../../../../common/constants'; +import { LabelPositionStylePropertyDescriptor } from '../../../../../../common/descriptor_types'; +import { LabelPositionProperty } from '../../properties/label_position_property'; + +const options = [ + { + value: LABEL_POSITIONS.TOP, + text: i18n.translate('xpack.maps.styles.labelPosition.top', { + defaultMessage: 'Top', + }), + }, + { + value: LABEL_POSITIONS.CENTER, + text: i18n.translate('xpack.maps.styles.labelPosition.center', { + defaultMessage: 'Center', + }), + }, + { + value: LABEL_POSITIONS.BOTTOM, + text: i18n.translate('xpack.maps.styles.labelBorderSize.bottom', { + defaultMessage: 'Bottom', + }), + }, +]; + +interface Props { + hasLabel: boolean; + handlePropertyChange: ( + propertyName: VECTOR_STYLES, + stylePropertyDescriptor: LabelPositionStylePropertyDescriptor + ) => void; + styleProperty: LabelPositionProperty; +} + +export function LabelPositionEditor({ hasLabel, handlePropertyChange, styleProperty }: Props) { + function onChange(e: ChangeEvent) { + handlePropertyChange(styleProperty.getStyleName(), { + options: { position: e.target.value as LABEL_POSITIONS }, + }); + } + + const disabled = !hasLabel || styleProperty.isDisabled(); + + const form = ( + + + + ); + + return !disabled ? ( + form + ) : ( + + {form} + + ); +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_border_size_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_border_size_editor.js index 2d192c2737c34..13d78ae21cb77 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_border_size_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_border_size_editor.js @@ -53,10 +53,7 @@ export function VectorStyleLabelBorderSizeEditor({ } const labelBorderSizeForm = ( - + { children: ReactElement; customStaticOptionLabel?: string; @@ -74,17 +82,11 @@ export class StylePropEditor extends Component< const options = [ { value: STYLE_TYPE.STATIC, - text: this.props.customStaticOptionLabel - ? this.props.customStaticOptionLabel - : i18n.translate('xpack.maps.styles.staticDynamicSelect.staticLabel', { - defaultMessage: 'Fixed', - }), + text: this.props.customStaticOptionLabel ? this.props.customStaticOptionLabel : FIXED_LABEL, }, { value: STYLE_TYPE.DYNAMIC, - text: i18n.translate('xpack.maps.styles.staticDynamicSelect.dynamicLabel', { - defaultMessage: 'By value', - }), + text: BY_VALUE_LABEL, }, ]; 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 61149cccb2a4c..e1b23aa28f5a1 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 @@ -16,6 +16,7 @@ import { VectorStyleSymbolizeAsEditor } from './symbol/vector_style_symbolize_as import { VectorStyleIconEditor } from './symbol/vector_style_icon_editor'; import { VectorStyleLabelEditor } from './label/vector_style_label_editor'; import { LabelZoomRangeEditor } from './label/label_zoom_range_editor'; +import { LabelPositionEditor } from './label/label_position_editor'; import { VectorStyleLabelBorderSizeEditor } from './label/vector_style_label_border_size_editor'; import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../vector_style_defaults'; @@ -49,6 +50,7 @@ import { LabelBorderSizeProperty } from '../properties/label_border_size_propert import { StaticTextProperty } from '../properties/static_text_property'; import { DynamicTextProperty } from '../properties/dynamic_text_property'; import { StaticSizeProperty } from '../properties/static_size_property'; +import { LabelPositionProperty } from '../properties/label_position_property'; import { LabelZoomRangeProperty } from '../properties/label_zoom_range_property'; import { IVectorLayer } from '../../../layers/vector_layer'; import { getHasLabel } from '../style_util'; @@ -263,7 +265,7 @@ export class VectorStyleEditor extends Component { ); } - _renderLabelProperties() { + _renderLabelProperties(isPoint: boolean) { const hasLabel = getHasLabel( this.props.styleProperties[VECTOR_STYLES.LABEL_TEXT] as | StaticTextProperty @@ -282,6 +284,7 @@ export class VectorStyleEditor extends Component { const labelBorderColorProperty = this.props.styleProperties[ VECTOR_STYLES.LABEL_BORDER_COLOR ] as IStyleProperty; + return ( { /> + {isPoint ? ( + <> + + + + ) : null} + { /> - {this._renderLabelProperties()} + {this._renderLabelProperties(true)} ); } @@ -508,7 +524,7 @@ export class VectorStyleEditor extends Component { {this._renderLineWidth()} - {this._renderLabelProperties()} + {this._renderLabelProperties(false)} ); } @@ -525,7 +541,7 @@ export class VectorStyleEditor extends Component { {this._renderLineWidth()} - {this._renderLabelProperties()} + {this._renderLabelProperties(false)} ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx index 13d93dffaaec0..dcaaa9ef227de 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx @@ -14,6 +14,7 @@ import { makeMbClampedNumberExpression } from '../../style_util'; import { FieldFormatter, HALF_MAKI_ICON_SIZE, + MB_LOOKUP_FUNCTION, VECTOR_STYLES, } from '../../../../../../common/constants'; import type { SizeDynamicOptions } from '../../../../../../common/descriptor_types'; @@ -76,9 +77,13 @@ export class DynamicSizeProperty extends DynamicStyleProperty= 1 const valueShift = rangeFieldMeta.min < 1 ? Math.abs(rangeFieldMeta.min) + 1 : 0; - const maxValueStopInput = isArea - ? Math.sqrt(rangeFieldMeta.max + valueShift) - : rangeFieldMeta.max; - const minValueStopInput = isArea - ? Math.sqrt(rangeFieldMeta.min + valueShift) - : rangeFieldMeta.min; - const maxRangeStopOutput = - this.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._isSymbolizedAsIcon - ? this._options.maxSize / HALF_MAKI_ICON_SIZE - : this._options.maxSize; - const minRangeStopOutput = - this.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._isSymbolizedAsIcon - ? this._options.minSize / HALF_MAKI_ICON_SIZE - : this._options.minSize; + const maxStopInput = isArea ? Math.sqrt(rangeFieldMeta.max + valueShift) : rangeFieldMeta.max; + const minStopInput = isArea ? Math.sqrt(rangeFieldMeta.min + valueShift) : rangeFieldMeta.min; + + const maxStopOutput = options?.maxStopOutput ? options.maxStopOutput : this.getMaxStopOutput(); + const minStopOutput = options?.minStopOutput ? options.minStopOutput : this.getMinStopOutput(); const invert = this._options.invert === undefined ? false : this._options.invert; function getStopsWithoutRange() { - return invert - ? [maxValueStopInput, minRangeStopOutput] - : [maxValueStopInput, maxRangeStopOutput]; + return invert ? [maxStopInput, minStopOutput] : [maxStopInput, maxStopOutput]; } function getStops() { return invert - ? [minValueStopInput, maxRangeStopOutput, maxValueStopInput, minRangeStopOutput] - : [minValueStopInput, minRangeStopOutput, maxValueStopInput, maxRangeStopOutput]; + ? [minStopInput, maxStopOutput, maxStopInput, minStopOutput] + : [minStopInput, minStopOutput, maxStopInput, maxStopOutput]; } const stops = rangeFieldMeta.min === rangeFieldMeta.max ? getStopsWithoutRange() : getStops(); const valueExpression = makeMbClampedNumberExpression({ - lookupFunction: this.getMbLookupFunction(), + lookupFunction: options?.forceFeatureProperties + ? MB_LOOKUP_FUNCTION.GET + : this.getMbLookupFunction(), maxValue: rangeFieldMeta.max, minValue: rangeFieldMeta.min, fieldName: this.getMbFieldName(), @@ -140,7 +136,19 @@ export class DynamicSizeProperty extends DynamicStyleProperty { + let layoutProperties: Record = {}; + const mockMbMap = { + setLayoutProperty: (layerId: string, propName: string, propValue: unknown) => { + layoutProperties[propName] = propValue; + }, + } as unknown as MbMap; + const mockStaticIconSize = { + isDynamic: () => { + return false; + }, + getOptions: () => { + return { + size: 24, + }; + }, + } as unknown as StaticSizeProperty; + const dynamicIconSize = new DynamicSizeProperty( + { + maxSize: 32, + minSize: 7, + } as unknown as SizeDynamicOptions, + VECTOR_STYLES.ICON_SIZE, + { + isValid: () => { + return true; + }, + getMbFieldName: () => { + return 'iconSizeField'; + }, + } as unknown as IField, + {} as unknown as IVectorLayer, + () => { + return null; + }, + false + ); + dynamicIconSize.getRangeFieldMeta = () => { + return { + min: 0, + max: 100, + delta: 100, + }; + }; + const mockStaticLabelSize = { + isDynamic: () => { + return false; + }, + getOptions: () => { + return { + size: 14, + }; + }, + } as unknown as StaticSizeProperty; + + beforeEach(() => { + layoutProperties = {}; + }); + + describe('center', () => { + test('should set center layout values when position is center', () => { + const labelPosition = new LabelPositionProperty( + { + position: LABEL_POSITIONS.CENTER, + }, + VECTOR_STYLES.LABEL_POSITION, + {} as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + false + ); + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'center', + 'text-offset': [0, 0], + }); + }); + }); + + describe('top', () => { + const options = { + position: LABEL_POSITIONS.TOP, + }; + + test('should fallback to center layout values when disabled', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + {} as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + false + ); + labelPosition.isDisabled = () => { + return true; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'center', + 'text-offset': [0, 0], + }); + }); + + test('should set layout values for static icon size', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + {} as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + false + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'bottom', + 'text-offset': [0, -1.7142857142857142], + }); + }); + + test('should set layout values when symbolized as icon with center anchor', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + { + isDynamic: () => { + return false; + }, + getSymbolAnchor: () => { + return 'center'; + }, + } as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + true + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'bottom', + 'text-offset': [0, -1.7142857142857142], + }); + }); + + test('should set layout values when symbolized as icon with bottom anchor', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + { + isDynamic: () => { + return false; + }, + getSymbolAnchor: () => { + return 'bottom'; + }, + } as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + true + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'bottom', + 'text-offset': [0, -3], + }); + }); + + test('should set layout values for dynamic icon size', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + {} as unknown as StaticIconProperty, + dynamicIconSize, + mockStaticLabelSize, + false + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'bottom', + 'text-offset': [ + 'interpolate', + ['linear'], + [ + 'sqrt', + [ + '+', + [ + 'coalesce', + [ + 'case', + ['==', ['get', 'iconSizeField'], null], + 0, + ['max', ['min', ['to-number', ['get', 'iconSizeField']], 100], 0], + ], + 0, + ], + 1, + ], + ], + 1, + ['literal', [0, -0.5]], + 10.04987562112089, + ['literal', [0, -2.2857142857142856]], + ], + }); + }); + }); + + describe('bottom', () => { + const options = { + position: LABEL_POSITIONS.BOTTOM, + }; + + test('should set layout values for static icon size', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + {} as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + false + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'top', + 'text-offset': [0, 1.7142857142857142], + }); + }); + + test('should set layout values when symbolized as icon with center anchor', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + { + isDynamic: () => { + return false; + }, + getSymbolAnchor: () => { + return 'center'; + }, + } as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + true + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'top', + 'text-offset': [0, 1.7142857142857142], + }); + }); + + test('should set layout values when symbolized as icon with bottom anchor', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + { + isDynamic: () => { + return false; + }, + getSymbolAnchor: () => { + return 'bottom'; + }, + } as unknown as StaticIconProperty, + mockStaticIconSize, + mockStaticLabelSize, + true + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'top', + 'text-offset': [0, 0], + }); + }); + + test('should set layout values for dynamic icon size', () => { + const labelPosition = new LabelPositionProperty( + options, + VECTOR_STYLES.LABEL_POSITION, + {} as unknown as StaticIconProperty, + dynamicIconSize, + mockStaticLabelSize, + false + ); + labelPosition.isDisabled = () => { + return false; + }; + labelPosition.syncLabelPositionWithMb('layerId', mockMbMap); + expect(layoutProperties).toEqual({ + 'text-anchor': 'top', + 'text-offset': [ + 'interpolate', + ['linear'], + [ + 'sqrt', + [ + '+', + [ + 'coalesce', + [ + 'case', + ['==', ['get', 'iconSizeField'], null], + 0, + ['max', ['min', ['to-number', ['get', 'iconSizeField']], 100], 0], + ], + 0, + ], + 1, + ], + ], + 1, + ['literal', [0, 0.5]], + 10.04987562112089, + ['literal', [0, 2.2857142857142856]], + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/label_position_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/label_position_property.ts new file mode 100644 index 0000000000000..e98a8caea3c83 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/label_position_property.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; +import { AbstractStyleProperty } from './style_property'; +import { LABEL_POSITIONS } from '../../../../../common/constants'; +import { LabelPositionStylePropertyDescriptor } from '../../../../../common/descriptor_types'; +import { FIELD_ORIGIN, VECTOR_STYLES } from '../../../../../common/constants'; +import { DEFAULT_ICON_SIZE, DEFAULT_LABEL_SIZE } from '../vector_style_defaults'; +import { DynamicIconProperty } from './dynamic_icon_property'; +import { StaticIconProperty } from './static_icon_property'; +import { DynamicSizeProperty } from './dynamic_size_property'; +import { StaticSizeProperty } from './static_size_property'; +import { getVectorStyleLabel } from '../components/get_vector_style_label'; +import { FIXED_LABEL, BY_VALUE_LABEL } from '../components/style_prop_editor'; + +export class LabelPositionProperty extends AbstractStyleProperty< + LabelPositionStylePropertyDescriptor['options'] +> { + private readonly _iconProperty: StaticIconProperty | DynamicIconProperty; + private readonly _iconSizeProperty: StaticSizeProperty | DynamicSizeProperty; + private readonly _labelSizeProperty: StaticSizeProperty | DynamicSizeProperty; + private readonly _isSymbolizedAsIcon: boolean; + + constructor( + options: LabelPositionStylePropertyDescriptor['options'], + styleName: VECTOR_STYLES, + iconProperty: StaticIconProperty | DynamicIconProperty, + iconSizeProperty: StaticSizeProperty | DynamicSizeProperty, + labelSizeProperty: StaticSizeProperty | DynamicSizeProperty, + isSymbolizedAsIcon: boolean + ) { + super(options, styleName); + this._iconProperty = iconProperty; + this._iconSizeProperty = iconSizeProperty; + this._labelSizeProperty = labelSizeProperty; + this._isSymbolizedAsIcon = isSymbolizedAsIcon; + } + + isDisabled() { + if (this._labelSizeProperty.isDynamic()) { + // dynamic label size not supported + return true; + } + + if (!this._iconSizeProperty.isDynamic() || !this._iconSizeProperty.isComplete()) { + // icon size is static so there are no concerns with using layout propery and feature-state + return false; + } + + // Label position can not be used in concunction with dynamic icon size from joins. + // Why? + // Label position sets layout properties. Layout properties do not support feature-state. + // Label position sets a layout property to the interpolate expression from dynamic icon size property + // This means that style data for dynamic icon size property can only be retrieved from feature.properties + // + return this._isIconSizeFromJoin(); + } + + getDisabledReason() { + if (this._labelSizeProperty.isDynamic()) { + return i18n.translate('xpack.maps.labelPosition.dynamicLabelSizeNotSupported', { + defaultMessage: `{labelPositionPropertyLabel} is not supported with '{byValueLabel}' {labelSizePropertyLabel}. Set {labelSizePropertyLabel} to '{fixedLabel}' to enable.`, + values: { + byValueLabel: BY_VALUE_LABEL.toLowerCase(), + fixedLabel: FIXED_LABEL.toLowerCase(), + labelSizePropertyLabel: getVectorStyleLabel(VECTOR_STYLES.LABEL_SIZE).toLowerCase(), + labelPositionPropertyLabel: getVectorStyleLabel(VECTOR_STYLES.LABEL_POSITION), + }, + }); + } + + return this._isIconSizeFromJoin() + ? i18n.translate('xpack.maps.labelPosition.iconSizeJoinFieldNotSupportMsg', { + defaultMessage: + '{labelPositionPropertyLabel} is not supported with {iconSizePropertyLabel} join field {iconSizeFieldName}. Set {iconSizePropertyLabel} to source field to enable.', + values: { + iconSizePropertyLabel: getVectorStyleLabel(VECTOR_STYLES.ICON_SIZE), + iconSizeFieldName: (this._iconSizeProperty as DynamicSizeProperty).getFieldName(), + labelPositionPropertyLabel: getVectorStyleLabel(VECTOR_STYLES.LABEL_POSITION), + }, + }) + : ''; + } + + syncLabelPositionWithMb(mbLayerId: string, mbMap: MbMap) { + if (this._options.position === LABEL_POSITIONS.CENTER || this.isDisabled()) { + mbMap.setLayoutProperty(mbLayerId, 'text-offset', [0, 0]); + mbMap.setLayoutProperty(mbLayerId, 'text-anchor', 'center'); + return; + } + + mbMap.setLayoutProperty( + mbLayerId, + 'text-anchor', + this._options.position === LABEL_POSITIONS.BOTTOM ? 'top' : 'bottom' + ); + + const labelSize = this._getLabelSize(); + + if ( + this._iconSizeProperty.isDynamic() && + this._iconSizeProperty.isComplete() && + (this._iconSizeProperty as DynamicSizeProperty).isSizeDynamicConfigComplete() + ) { + const dynamicIconSizeOptions = (this._iconSizeProperty as DynamicSizeProperty).getOptions(); + const interpolateExpression = ( + this._iconSizeProperty as DynamicSizeProperty + ).getMbSizeExpression({ + forceFeatureProperties: true, + maxStopOutput: ['literal', this._getTextOffset(dynamicIconSizeOptions.maxSize, labelSize)], + minStopOutput: ['literal', this._getTextOffset(dynamicIconSizeOptions.minSize, labelSize)], + }); + mbMap.setLayoutProperty(mbLayerId, 'text-offset', interpolateExpression); + return; + } + + const iconSize = !this._iconSizeProperty.isDynamic() + ? (this._iconSizeProperty as StaticSizeProperty).getOptions().size + : DEFAULT_ICON_SIZE; + mbMap.setLayoutProperty(mbLayerId, 'text-offset', this._getTextOffset(iconSize, labelSize)); + } + + // https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/#layout-symbol-text-offset + _getTextOffset(symbolSize: number, labelSize: number) { + const ems = symbolSize / labelSize; + // Positive values indicate right and down, while negative values indicate left and up + const verticalTextOffset = this._options.position === LABEL_POSITIONS.BOTTOM ? ems : ems * -1; + return [0, verticalTextOffset * this._getIconScale()]; + } + + _getIconScale() { + if (!this._isSymbolizedAsIcon) { + return 1; + } + + const iconAnchor = !this._iconProperty.isDynamic() + ? (this._iconProperty as StaticIconProperty).getSymbolAnchor() + : 'center'; + + if (iconAnchor === 'center') { + return 1; + } + + // using scaling factor of 1.75 + // scaling factor of 1.5 is too small - labels touch top of icon + // scaling factor of 2 is too big - labels are too far above icon + return this._options.position === LABEL_POSITIONS.TOP ? 1.75 : 0; + } + + _getLabelSize() { + return !this._labelSizeProperty.isDynamic() + ? (this._labelSizeProperty as StaticSizeProperty).getOptions().size + : DEFAULT_LABEL_SIZE; + } + + _isIconSizeFromJoin() { + return ( + this._iconSizeProperty.isDynamic() && + (this._iconSizeProperty as DynamicSizeProperty).getFieldOrigin() === FIELD_ORIGIN.JOIN + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts index 83cab4633d212..106d8df52c7f3 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts @@ -12,8 +12,11 @@ import { IconStaticOptions } from '../../../../../common/descriptor_types'; export class StaticIconProperty extends StaticStyleProperty { syncIconWithMb(symbolLayerId: string, mbMap: MbMap) { - const symbolId = this._options.value; - mbMap.setLayoutProperty(symbolLayerId, 'icon-anchor', getMakiSymbolAnchor(symbolId)); - mbMap.setLayoutProperty(symbolLayerId, 'icon-image', symbolId); + mbMap.setLayoutProperty(symbolLayerId, 'icon-anchor', this.getSymbolAnchor()); + mbMap.setLayoutProperty(symbolLayerId, 'icon-image', this._options.value); + } + + getSymbolAnchor() { + return getMakiSymbolAnchor(this._options.value); } } 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 b57f8adcddd8e..3b64d0960628c 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 @@ -41,6 +41,7 @@ import { StaticTextProperty } from './properties/static_text_property'; import { DynamicTextProperty } from './properties/dynamic_text_property'; import { LabelZoomRangeProperty } from './properties/label_zoom_range_property'; import { LabelBorderSizeProperty } from './properties/label_border_size_property'; +import { LabelPositionProperty } from './properties/label_position_property'; import { extractColorFromStyleProperty } from './components/legend/extract_color_from_style_property'; import { SymbolizeAsProperty } from './properties/symbolize_as_property'; import { StaticIconProperty } from './properties/static_icon_property'; @@ -170,19 +171,20 @@ export class VectorStyle implements IVectorStyle { private readonly _source: IVectorSource; private readonly _styleMeta: StyleMeta; - private readonly _symbolizeAsStyleProperty: SymbolizeAsProperty; - private readonly _lineColorStyleProperty: StaticColorProperty | DynamicColorProperty; - private readonly _fillColorStyleProperty: StaticColorProperty | DynamicColorProperty; - private readonly _lineWidthStyleProperty: StaticSizeProperty | DynamicSizeProperty; - private readonly _iconStyleProperty: StaticIconProperty | DynamicIconProperty; - private readonly _iconSizeStyleProperty: StaticSizeProperty | DynamicSizeProperty; - private readonly _iconOrientationProperty: StaticOrientationProperty | DynamicOrientationProperty; - private readonly _labelStyleProperty: StaticTextProperty | DynamicTextProperty; - private readonly _labelZoomRangeProperty: LabelZoomRangeProperty; - private readonly _labelSizeStyleProperty: StaticSizeProperty | DynamicSizeProperty; - private readonly _labelColorStyleProperty: StaticColorProperty | DynamicColorProperty; - private readonly _labelBorderColorStyleProperty: StaticColorProperty | DynamicColorProperty; - private readonly _labelBorderSizeStyleProperty: LabelBorderSizeProperty; + private readonly _symbolizeAs: SymbolizeAsProperty; + private readonly _lineColor: StaticColorProperty | DynamicColorProperty; + private readonly _fillColor: StaticColorProperty | DynamicColorProperty; + private readonly _lineWidth: StaticSizeProperty | DynamicSizeProperty; + private readonly _icon: StaticIconProperty | DynamicIconProperty; + private readonly _iconSize: StaticSizeProperty | DynamicSizeProperty; + private readonly _iconOrientation: StaticOrientationProperty | DynamicOrientationProperty; + private readonly _label: StaticTextProperty | DynamicTextProperty; + private readonly _labelZoomRange: LabelZoomRangeProperty; + private readonly _labelSize: StaticSizeProperty | DynamicSizeProperty; + private readonly _labelColor: StaticColorProperty | DynamicColorProperty; + private readonly _labelBorderColor: StaticColorProperty | DynamicColorProperty; + private readonly _labelBorderSize: LabelBorderSizeProperty; + private readonly _labelPosition: LabelPositionProperty; static createDescriptor( properties: Partial = {}, @@ -218,65 +220,69 @@ export class VectorStyle implements IVectorStyle { this._styleMeta = new StyleMeta(this._descriptor.__styleMeta); - this._symbolizeAsStyleProperty = new SymbolizeAsProperty( + this._symbolizeAs = new SymbolizeAsProperty( this._descriptor.properties[VECTOR_STYLES.SYMBOLIZE_AS].options, VECTOR_STYLES.SYMBOLIZE_AS ); - this._lineColorStyleProperty = this._makeColorProperty( + this._lineColor = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], VECTOR_STYLES.LINE_COLOR, chartsPaletteServiceGetColor ); - this._fillColorStyleProperty = this._makeColorProperty( + this._fillColor = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.FILL_COLOR], VECTOR_STYLES.FILL_COLOR, chartsPaletteServiceGetColor ); - this._lineWidthStyleProperty = this._makeSizeProperty( + this._lineWidth = this._makeSizeProperty( this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH], VECTOR_STYLES.LINE_WIDTH, - this._symbolizeAsStyleProperty.isSymbolizedAsIcon() + this._symbolizeAs.isSymbolizedAsIcon() ); - this._iconStyleProperty = this._makeIconProperty( - this._descriptor.properties[VECTOR_STYLES.ICON] - ); - this._iconSizeStyleProperty = this._makeSizeProperty( + this._icon = this._makeIconProperty(this._descriptor.properties[VECTOR_STYLES.ICON]); + this._iconSize = this._makeSizeProperty( this._descriptor.properties[VECTOR_STYLES.ICON_SIZE], VECTOR_STYLES.ICON_SIZE, - this._symbolizeAsStyleProperty.isSymbolizedAsIcon() + this._symbolizeAs.isSymbolizedAsIcon() ); - this._iconOrientationProperty = this._makeOrientationProperty( + this._iconOrientation = this._makeOrientationProperty( this._descriptor.properties[VECTOR_STYLES.ICON_ORIENTATION], VECTOR_STYLES.ICON_ORIENTATION ); - this._labelStyleProperty = this._makeLabelProperty( - this._descriptor.properties[VECTOR_STYLES.LABEL_TEXT] - ); - this._labelZoomRangeProperty = new LabelZoomRangeProperty( + this._label = this._makeLabelProperty(this._descriptor.properties[VECTOR_STYLES.LABEL_TEXT]); + this._labelZoomRange = new LabelZoomRangeProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_ZOOM_RANGE].options, VECTOR_STYLES.LABEL_ZOOM_RANGE, layer.getMinZoom(), layer.getMaxZoom() ); - this._labelSizeStyleProperty = this._makeSizeProperty( + this._labelSize = this._makeSizeProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_SIZE], VECTOR_STYLES.LABEL_SIZE, - this._symbolizeAsStyleProperty.isSymbolizedAsIcon() + this._symbolizeAs.isSymbolizedAsIcon() ); - this._labelColorStyleProperty = this._makeColorProperty( + this._labelColor = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_COLOR], VECTOR_STYLES.LABEL_COLOR, chartsPaletteServiceGetColor ); - this._labelBorderColorStyleProperty = this._makeColorProperty( + this._labelBorderColor = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR], VECTOR_STYLES.LABEL_BORDER_COLOR, chartsPaletteServiceGetColor ); - this._labelBorderSizeStyleProperty = new LabelBorderSizeProperty( + this._labelBorderSize = new LabelBorderSizeProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE].options, VECTOR_STYLES.LABEL_BORDER_SIZE, - this._labelSizeStyleProperty + this._labelSize + ); + this._labelPosition = new LabelPositionProperty( + this._descriptor.properties[VECTOR_STYLES.LABEL_POSITION].options, + VECTOR_STYLES.LABEL_POSITION, + this._icon, + this._iconSize, + this._labelSize, + this._symbolizeAs.isSymbolizedAsIcon() ); } @@ -459,26 +465,27 @@ export class VectorStyle implements IVectorStyle { getAllStyleProperties(): Array> { return [ - this._symbolizeAsStyleProperty, - this._iconStyleProperty, - this._lineColorStyleProperty, - this._fillColorStyleProperty, - this._lineWidthStyleProperty, - this._iconSizeStyleProperty, - this._iconOrientationProperty, - this._labelStyleProperty, - this._labelZoomRangeProperty, - this._labelSizeStyleProperty, - this._labelColorStyleProperty, - this._labelBorderColorStyleProperty, - this._labelBorderSizeStyleProperty, + this._symbolizeAs, + this._icon, + this._lineColor, + this._fillColor, + this._lineWidth, + this._iconSize, + this._iconOrientation, + this._label, + this._labelZoomRange, + this._labelSize, + this._labelColor, + this._labelBorderColor, + this._labelBorderSize, + this._labelPosition, ]; } _hasBorder() { - return this._lineWidthStyleProperty.isDynamic() - ? this._lineWidthStyleProperty.isComplete() - : (this._lineWidthStyleProperty as StaticSizeProperty).getOptions().size !== 0; + return this._lineWidth.isDynamic() + ? this._lineWidth.isComplete() + : (this._lineWidth as StaticSizeProperty).getOptions().size !== 0; } renderEditor( @@ -609,9 +616,9 @@ export class VectorStyle implements IVectorStyle { } _getSymbolId() { - return this.arePointsSymbolizedAsCircles() || this._iconStyleProperty.isDynamic() + return this.arePointsSymbolizedAsCircles() || this._icon.isDynamic() ? undefined - : (this._iconStyleProperty as StaticIconProperty).getOptions().value; + : (this._icon as StaticIconProperty).getOptions().value; } _getIconMeta( @@ -680,11 +687,11 @@ export class VectorStyle implements IVectorStyle { } isUsingCustomIcon(symbolId: string) { - if (this._iconStyleProperty.isDynamic()) { - const { customIconStops } = this._iconStyleProperty.getOptions() as IconDynamicOptions; + if (this._icon.isDynamic()) { + const { customIconStops } = this._icon.getOptions() as IconDynamicOptions; return customIconStops ? customIconStops.some(({ icon }) => icon === symbolId) : false; } - const { value } = this._iconStyleProperty.getOptions() as IconStaticOptions; + const { value } = this._icon.getOptions() as IconStaticOptions; return value === symbolId; } @@ -780,11 +787,11 @@ export class VectorStyle implements IVectorStyle { } arePointsSymbolizedAsCircles() { - return !this._symbolizeAsStyleProperty.isSymbolizedAsIcon(); + return !this._symbolizeAs.isSymbolizedAsIcon(); } hasLabels() { - return getHasLabel(this._labelStyleProperty); + return getHasLabel(this._label); } setMBPaintProperties({ @@ -798,9 +805,9 @@ export class VectorStyle implements IVectorStyle { fillLayerId: string; lineLayerId: string; }) { - this._fillColorStyleProperty.syncFillColorWithMb(fillLayerId, mbMap, alpha); - this._lineColorStyleProperty.syncLineColorWithMb(lineLayerId, mbMap, alpha); - this._lineWidthStyleProperty.syncLineWidthWithMb(lineLayerId, mbMap); + this._fillColor.syncFillColorWithMb(fillLayerId, mbMap, alpha); + this._lineColor.syncLineColorWithMb(lineLayerId, mbMap, alpha); + this._lineWidth.syncLineWidthWithMb(lineLayerId, mbMap); } setMBPaintPropertiesForPoints({ @@ -812,13 +819,12 @@ export class VectorStyle implements IVectorStyle { mbMap: MbMap; pointLayerId: string; }) { - this._fillColorStyleProperty.syncCircleColorWithMb(pointLayerId, mbMap, alpha); - this._lineColorStyleProperty.syncCircleStrokeWithMb(pointLayerId, mbMap, alpha); + this._fillColor.syncCircleColorWithMb(pointLayerId, mbMap, alpha); + this._lineColor.syncCircleStrokeWithMb(pointLayerId, mbMap, alpha); const hasNoRadius = - !this._iconSizeStyleProperty.isDynamic() && - (this._iconSizeStyleProperty as StaticSizeProperty).getOptions().size === 0; - this._lineWidthStyleProperty.syncCircleStrokeWidthWithMb(pointLayerId, mbMap, hasNoRadius); - this._iconSizeStyleProperty.syncCircleRadiusWithMb(pointLayerId, mbMap); + !this._iconSize.isDynamic() && (this._iconSize as StaticSizeProperty).getOptions().size === 0; + this._lineWidth.syncCircleStrokeWidthWithMb(pointLayerId, mbMap, hasNoRadius); + this._iconSize.syncCircleRadiusWithMb(pointLayerId, mbMap); } setMBPropertiesForLabelText({ @@ -830,12 +836,13 @@ export class VectorStyle implements IVectorStyle { mbMap: MbMap; textLayerId: string; }) { - this._labelStyleProperty.syncTextFieldWithMb(textLayerId, mbMap); - this._labelZoomRangeProperty.syncLabelZoomRange(textLayerId, mbMap); - this._labelColorStyleProperty.syncLabelColorWithMb(textLayerId, mbMap, alpha); - this._labelSizeStyleProperty.syncLabelSizeWithMb(textLayerId, mbMap); - this._labelBorderSizeStyleProperty.syncLabelBorderSizeWithMb(textLayerId, mbMap); - this._labelBorderColorStyleProperty.syncLabelBorderColorWithMb(textLayerId, mbMap); + this._label.syncTextFieldWithMb(textLayerId, mbMap); + this._labelZoomRange.syncLabelZoomRange(textLayerId, mbMap); + this._labelColor.syncLabelColorWithMb(textLayerId, mbMap, alpha); + this._labelSize.syncLabelSizeWithMb(textLayerId, mbMap); + this._labelBorderSize.syncLabelBorderSizeWithMb(textLayerId, mbMap); + this._labelPosition.syncLabelPositionWithMb(textLayerId, mbMap); + this._labelBorderColor.syncLabelBorderColorWithMb(textLayerId, mbMap); } setMBSymbolPropertiesForPoints({ @@ -851,13 +858,13 @@ export class VectorStyle implements IVectorStyle { mbMap.setPaintProperty(symbolLayerId, 'icon-opacity', alpha); mbMap.setLayoutProperty(symbolLayerId, 'icon-allow-overlap', true); - this._iconStyleProperty.syncIconWithMb(symbolLayerId, mbMap); + this._icon.syncIconWithMb(symbolLayerId, mbMap); // icon-color is only supported on SDF icons. - this._fillColorStyleProperty.syncIconColorWithMb(symbolLayerId, mbMap); - this._lineColorStyleProperty.syncHaloBorderColorWithMb(symbolLayerId, mbMap); - this._lineWidthStyleProperty.syncHaloWidthWithMb(symbolLayerId, mbMap); - this._iconSizeStyleProperty.syncIconSizeWithMb(symbolLayerId, mbMap); - this._iconOrientationProperty.syncIconRotationWithMb(symbolLayerId, mbMap); + this._fillColor.syncIconColorWithMb(symbolLayerId, mbMap); + this._lineColor.syncHaloBorderColorWithMb(symbolLayerId, mbMap); + this._lineWidth.syncHaloWidthWithMb(symbolLayerId, mbMap); + this._iconSize.syncIconSizeWithMb(symbolLayerId, mbMap); + this._iconOrientation.syncIconRotationWithMb(symbolLayerId, mbMap); } _makeField(fieldDescriptor?: StylePropertyField): IField | null { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts index 7556c25dd4cf9..d7b348efa500e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts @@ -8,6 +8,7 @@ import { DEFAULT_ICON, LABEL_BORDER_SIZES, + LABEL_POSITIONS, MAX_ZOOM, MIN_ZOOM, SYMBOLIZE_AS_TYPES, @@ -141,6 +142,11 @@ export function getDefaultStaticProperties( size: LABEL_BORDER_SIZES.SMALL, }, }, + [VECTOR_STYLES.LABEL_POSITION]: { + options: { + position: LABEL_POSITIONS.CENTER, + }, + }, }; } @@ -273,5 +279,10 @@ export function getDefaultDynamicProperties(): VectorStylePropertiesDescriptor { size: LABEL_BORDER_SIZES.SMALL, }, }, + [VECTOR_STYLES.LABEL_POSITION]: { + options: { + position: LABEL_POSITIONS.CENTER, + }, + }, }; } diff --git a/x-pack/plugins/ux/e2e/journeys/index.ts b/x-pack/plugins/ux/e2e/journeys/index.ts index 1377dda843dc4..cb6a5b4145932 100644 --- a/x-pack/plugins/ux/e2e/journeys/index.ts +++ b/x-pack/plugins/ux/e2e/journeys/index.ts @@ -6,7 +6,7 @@ */ export * from './core_web_vitals'; -export * from './page_views'; +// export * from './page_views'; export * from './url_ux_query.journey'; export * from './ux_js_errors.journey'; export * from './ux_client_metrics.journey'; diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts index a7882e7d21d33..beef30007ffce 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/__mocks__/regions_layer.mock.ts @@ -65,6 +65,7 @@ export const mockLayerList = [ iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, labelText: { type: 'STATIC', options: { value: '' } }, labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelPosition: { options: { position: 'CENTER' } }, labelSize: { type: 'STATIC', options: { size: 14 } }, labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, labelZoomRange: { @@ -136,6 +137,7 @@ export const mockLayerList = [ iconOrientation: { type: 'STATIC', options: { orientation: 0 } }, labelText: { type: 'STATIC', options: { value: '' } }, labelColor: { type: 'STATIC', options: { color: '#000000' } }, + labelPosition: { options: { position: 'CENTER' } }, labelSize: { type: 'STATIC', options: { size: 14 } }, labelBorderColor: { type: 'STATIC', options: { color: '#FFFFFF' } }, labelZoomRange: { diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts index eca4df56ff6b4..4ec159b8dcab4 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/visitor_breakdown_map/use_layer_list.ts @@ -15,6 +15,7 @@ import { COLOR_MAP_TYPE, FIELD_ORIGIN, LABEL_BORDER_SIZES, + LABEL_POSITIONS, LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, @@ -118,6 +119,11 @@ export function useLayerList() { options: { orientation: 0 }, }, labelText: { type: STYLE_TYPE.STATIC, options: { value: '' } }, + labelPosition: { + options: { + position: LABEL_POSITIONS.CENTER, + }, + }, labelZoomRange: { options: { useLayerZoomRange: true, From 73e9386247f4ce1772b3d8a5fabc2781fb3aee16 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 29 Nov 2022 11:10:09 -0500 Subject: [PATCH 05/11] [Security Solution] Use default time range if one can't be determined from query (#145651) ## Summary Currently if an investigation guide contains markup providers that do not match any events, an empty string is used as a date, resulting in https://github.com/elastic/kibana/issues/145521 . This pr fixes that issue by falling back to the users default settings in any case where a time range cannot be determined. image ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../table/investigate_in_timeline_button.tsx | 8 +- .../plugins/insight/index.test.tsx | 90 +++++++++++++++++++ .../markdown_editor/plugins/insight/index.tsx | 26 ++++-- 3 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 3c095c5ed87da..ef82b35e64d07 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -23,14 +23,18 @@ import { useCreateTimeline } from '../../../../timelines/components/timeline/pro import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; -export const InvestigateInTimelineButton: React.FunctionComponent<{ +export interface InvestigateInTimelineButtonProps { asEmptyButton: boolean; dataProviders: DataProvider[] | null; filters?: Filter[] | null; timeRange?: TimeRange; keepDataView?: boolean; isDisabled?: boolean; -}> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { +} + +export const InvestigateInTimelineButton: React.FunctionComponent< + InvestigateInTimelineButtonProps +> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { const dispatch = useDispatch(); const getDataViewsSelector = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx new file mode 100644 index 0000000000000..411c72d1d41af --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import moment from 'moment'; +import { TestProviders } from '../../../../mock'; +import { + DEFAULT_FROM, + DEFAULT_APP_TIME_RANGE, + DEFAULT_TO, +} from '../../../../../../common/constants'; +import { KibanaServices } from '../../../../lib/kibana'; +import type { DefaultTimeRangeSetting } from '../../../../utils/default_date_settings'; +import { renderer as Renderer } from '.'; +import type { InvestigateInTimelineButtonProps } from '../../../event_details/table/investigate_in_timeline_button'; + +jest.mock('../../../../lib/kibana'); +const mockGetServices = KibanaServices.get as jest.Mock; + +jest.mock('../../../event_details/table/investigate_in_timeline_button', () => { + const originalModule = jest.requireActual( + '../../../event_details/table/investigate_in_timeline_button' + ); + return { + ...originalModule, + InvestigateInTimelineButton: function InvestigateInTimelineButton( + props: InvestigateInTimelineButtonProps + ) { + return ( +
+ ); + }, + }; +}); +const mockTimeRange = ( + timeRange: DefaultTimeRangeSetting = { from: DEFAULT_FROM, to: DEFAULT_TO } +) => { + mockGetServices.mockImplementation(() => ({ + uiSettings: { + get: (key: string) => { + switch (key) { + case DEFAULT_APP_TIME_RANGE: + return timeRange; + default: + throw new Error(`Unexpected config key: ${key}`); + } + }, + }, + })); +}; + +describe('insight component renderer', () => { + beforeEach(() => { + mockTimeRange(null); + }); + it('renders correctly with valid date strings with no timestamp from results', () => { + render( + + + + ); + const timelineButton = screen.getByTestId('insight-investigate-in-timeline-button'); + const relativeTo = timelineButton.getAttribute('data-timerange-to') || ''; + const relativeFrom = timelineButton.getAttribute('data-timerange-from') || ''; + expect(timelineButton).toHaveAttribute('data-timerange-kind', 'relative'); + try { + const toDate = new Date(relativeTo); + const fromDate = new Date(relativeFrom); + expect(moment(toDate).isValid()).toBe(true); + expect(moment(fromDate).isValid()).toBe(true); + } catch { + expect(false).toBe(true); + } + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 081ab17aa0dff..3ed9a8ab41f26 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -15,7 +15,8 @@ import { useInsightQuery } from './use_insight_query'; import { useInsightDataProviders } from './use_insight_data_providers'; import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button'; -import type { AbsoluteTimeRange } from '../../../../store/inputs/model'; +import { getTimeRangeSettings } from '../../../../utils/default_date_settings'; +import type { TimeRange } from '../../../../store/inputs/model'; interface InsightComponentProps { label?: string; @@ -117,12 +118,23 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro const { totalCount, isQueryLoading, oldestTimestamp, hasError } = useInsightQuery({ dataProviders, }); - const timerange: AbsoluteTimeRange = useMemo(() => { - return { - kind: 'absolute', - from: oldestTimestamp ?? '', - to: new Date().toISOString(), - }; + const timerange: TimeRange = useMemo(() => { + if (oldestTimestamp != null) { + return { + kind: 'absolute', + from: oldestTimestamp, + to: new Date().toISOString(), + }; + } else { + const { to, from, fromStr, toStr } = getTimeRangeSettings(); + return { + kind: 'relative', + to, + from, + fromStr, + toStr, + }; + } }, [oldestTimestamp]); if (isQueryLoading) { return ; From 6c2087cc51448f3d79f974121fcede6ac0e84f16 Mon Sep 17 00:00:00 2001 From: Yngrid Coello Date: Tue, 29 Nov 2022 17:13:11 +0100 Subject: [PATCH 06/11] [APM] Mark operations breakdown as beta (#146535) Closes https://github.com/elastic/kibana/issues/145537. **Before** image **After** image --- .../templates/dependency_detail_template.tsx | 20 ++++++++++--------- .../observability/server/ui_settings.ts | 10 +++++++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx index 57ff4ba98a04a..f7cf9734dcdce 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/dependency_detail_template.tsx @@ -6,21 +6,22 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ApmMainTemplate } from './apm_main_template'; -import { SpanIcon } from '../../shared/span_icon'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { useTimeRange } from '../../../hooks/use_time_range'; -import { useFetcher } from '../../../hooks/use_fetcher'; -import { useApmRouter } from '../../../hooks/use_apm_router'; -import { useApmRoutePath } from '../../../hooks/use_apm_route_path'; -import { SearchBar } from '../../shared/search_bar'; +import React from 'react'; import { getKueryBarBoolFilter, kueryBarPlaceholder, } from '../../../../common/dependencies'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../hooks/use_apm_router'; +import { useApmRoutePath } from '../../../hooks/use_apm_route_path'; +import { useFetcher } from '../../../hooks/use_fetcher'; import { useOperationBreakdownEnabledSetting } from '../../../hooks/use_operations_breakdown_enabled_setting'; +import { useTimeRange } from '../../../hooks/use_time_range'; +import { BetaBadge } from '../../shared/beta_badge'; +import { SearchBar } from '../../shared/search_bar'; +import { SpanIcon } from '../../shared/span_icon'; +import { ApmMainTemplate } from './apm_main_template'; interface Props { children: React.ReactNode; @@ -90,6 +91,7 @@ export function DependencyDetailTemplate({ children }: Props) { isSelected: path === '/dependencies/operations' || path === '/dependencies/operation', + append: , }, ] : []; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 4b9211ca021ed..e576e232d8b85 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -29,6 +29,10 @@ import { enableInfrastructureHostsView, } from '../common/ui_settings_keys'; +const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', { + defaultMessage: 'beta', +}); + const technicalPreviewLabel = i18n.translate( 'xpack.observability.uiSettings.technicalPreviewLabel', { defaultMessage: 'technical preview' } @@ -223,14 +227,14 @@ export const uiSettings: Record = { }), description: i18n.translate('xpack.observability.apmOperationsBreakdownDescription', { defaultMessage: - '{technicalPreviewLabel} Enable the APM Operations Breakdown feature, that displays aggregates for backend operations. {feedbackLink}.', + '{betaLabel} Enable the APM Operations Breakdown feature, that displays aggregates for backend operations. {feedbackLink}.', values: { - technicalPreviewLabel: `[${technicalPreviewLabel}]`, + betaLabel: `[${betaLabel}]`, feedbackLink: feedbackLink({ href: 'https://ela.st/feedback-operations-breakdown' }), }, }), schema: schema.boolean(), - value: false, + value: true, requiresPageReload: true, type: 'boolean', showInLabs: true, From 5582afe4fdbed101ef1c6009d1d440cc415cc0cb Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Tue, 29 Nov 2022 08:18:59 -0800 Subject: [PATCH 07/11] [Security Solution][Alerts] Fix suppression icon in rule name for preview and popover (#145587) ## Summary https://github.com/elastic/kibana/issues/145544 - Suppression icon should show up in rule name even when `kibana.alert.suppression.docs_count` column is not included in the table https://github.com/elastic/kibana/issues/145669 - Rule name cell popover formatting No issue - adds the rule name icon for rule preview table `props.data` is the fetched columns, `props.ecsData` always has the fields listed in `requiredFieldsForActions` so we can use `kibana.alert.suppression.docs_count` even when that column is missing. --- .../preview_table_cell_renderer.test.tsx | 167 ------------------ .../preview_table_cell_renderer.tsx | 97 +--------- .../render_cell_value.tsx | 35 ++-- .../cell_rendering/default_cell_renderer.tsx | 3 +- x-pack/plugins/timelines/common/ecs/index.ts | 3 + .../common/types/timeline/cells/index.ts | 1 + 6 files changed, 24 insertions(+), 282 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx deleted file mode 100644 index 973a0f5f70345..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx +++ /dev/null @@ -1,167 +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 { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React from 'react'; - -import { mockBrowserFields } from '../../../../common/containers/source/mock'; -import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; -import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock'; -import { PreviewTableCellRenderer } from './preview_table_cell_renderer'; -import { getColumnRenderer } from '../../../../timelines/components/timeline/body/renderers/get_column_renderer'; -import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; -import type { BrowserFields } from '@kbn/timelines-plugin/common/search_strategy'; -import type { Ecs } from '../../../../../common/ecs'; -import { columnRenderers } from '../../../../timelines/components/timeline/body/renderers'; -import { TimelineId } from '../../../../../common/types'; - -jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../timelines/components/timeline/body/renderers/get_column_renderer'); - -const getColumnRendererMock = getColumnRenderer as jest.Mock; -const mockImplementation = { - renderColumn: jest.fn(), -}; - -describe('PreviewTableCellRenderer', () => { - const columnId = '@timestamp'; - const eventId = '_id-123'; - const isExpandable = true; - const isExpanded = true; - const linkValues = ['foo', 'bar', '@baz']; - const rowIndex = 3; - const colIndex = 0; - const setCellProps = jest.fn(); - const scopeId = TimelineId.test; - const ecsData = {} as Ecs; - const browserFields = {} as BrowserFields; - - beforeEach(() => { - jest.clearAllMocks(); - getColumnRendererMock.mockImplementation(() => mockImplementation); - }); - - test('it invokes `getColumnRenderer` with the expected arguments', () => { - const data = cloneDeep(mockTimelineData[0].data); - const header = cloneDeep(defaultHeaders[0]); - const isDetails = true; - - mount( - - - - - - - - ); - - expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data); - }); - - test('if in tgrid expanded value, it invokes `renderColumn` with the expected arguments', () => { - const data = cloneDeep(mockTimelineData[0].data); - const header = cloneDeep(defaultHeaders[0]); - const isDetails = true; - const truncate = isDetails ? false : true; - - mount( - - - - - - - - ); - - expect(mockImplementation.renderColumn).toBeCalledWith({ - asPlainText: false, - columnName: header.id, - ecsData, - eventId, - field: header, - isDetails, - isDraggable: true, - linkValues, - rowRenderers: undefined, - scopeId, - truncate, - values: ['2018-11-05T19:03:25.937Z'], - }); - }); - - test('if in tgrid expanded value, it does not render any actions', () => { - const data = cloneDeep(mockTimelineData[0].data); - const header = cloneDeep(defaultHeaders[1]); - const isDetails = true; - const id = 'event.severity'; - const wrapper = mount( - - - - - - - - ); - - expect( - wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists() - ).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx index d8e2017f4fa90..e5d4821b0ffbb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx @@ -5,102 +5,11 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import type React from 'react'; import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; import type { CellValueElementProps } from '@kbn/timelines-plugin/common'; -import { StyledContent } from '../../../../common/lib/cell_actions/expanded_cell_value_actions'; -import { getLinkColumnDefinition } from '../../../../common/lib/cell_actions/helpers'; -import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns'; -import { columnRenderers } from '../../../../timelines/components/timeline/body/renderers'; -import { getColumnRenderer } from '../../../../timelines/components/timeline/body/renderers/get_column_renderer'; +import { RenderCellValue } from '../../../configurations/security_solution_detections'; export const PreviewRenderCellValue: React.FC< EuiDataGridCellValueElementProps & CellValueElementProps -> = ({ - columnId, - data, - ecsData, - eventId, - globalFilters, - header, - isDetails, - isDraggable, - isExpandable, - isExpanded, - linkValues, - rowIndex, - colIndex, - rowRenderers, - setCellProps, - scopeId, - truncate, -}) => ( - -); - -export const PreviewTableCellRenderer: React.FC = ({ - data, - ecsData, - eventId, - header, - isDetails, - isDraggable, - isTimeline, - linkValues, - rowRenderers, - scopeId, - truncate, - key, -}) => { - const asPlainText = useMemo(() => { - return getLinkColumnDefinition(header.id, header.type, undefined) !== undefined && !isTimeline; - }, [header.id, header.type, isTimeline]); - - const values = useGetMappedNonEcsValue({ - data, - fieldName: header.id, - }); - const styledContentClassName = isDetails - ? 'eui-textBreakWord' - : 'eui-displayInlineBlock eui-textTruncate'; - - return ( - <> - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - asPlainText, - columnName: header.id, - ecsData, - eventId, - field: header, - isDetails, - isDraggable, - linkValues, - rowRenderers, - scopeId, - truncate, - values, - key, - })} - - - ); -}; +> = (props) => RenderCellValue({ ...props, enableActions: false }); diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index e437ad1120c04..4df1eb412b2b3 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -6,10 +6,8 @@ */ import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import { find } from 'lodash/fp'; import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step'; import { isDetectionsAlertsTable } from '../../../common/components/top_n/helpers'; import { @@ -26,10 +24,6 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { SUPPRESSED_ALERT_TOOLTIP } from './translations'; -const SuppressedAlertIconWrapper = styled.div` - display: inline-flex; -`; - /** * This implementation of `EuiDataGrid`'s `renderCellValue` * accepts `EuiDataGridCellValueElementProps`, plus `data` @@ -48,7 +42,7 @@ export const RenderCellValue: React.FC 0 ? ( - - - - -   - {component} - + suppressionCount && + parseInt(suppressionCount[0], 10) > 0 ? ( + + + + + + + {component} + ) : ( component ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx index 01c06946b5343..0e1413f4d421c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -37,6 +37,7 @@ export const DefaultCellRenderer: React.FC = ({ scopeId, truncate, closeCellPopover, + enableActions = true, }) => { const asPlainText = useMemo(() => { return getLinkColumnDefinition(header.id, header.type, undefined) !== undefined && !isTimeline; @@ -67,7 +68,7 @@ export const DefaultCellRenderer: React.FC = ({ values, })} - {isDetails && hasCellActions(header.id) && ( + {enableActions && isDetails && hasCellActions(header.id) && ( & { rule?: Exclude & { parameters: Record; uuid: string[] }; building_block_type?: string[]; workflow_status?: string[]; + suppression?: { + docs_count: string[]; + }; }; export interface Ecs { _id: string; diff --git a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts index 52130cf52354d..2cf52c24b5b5e 100644 --- a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts @@ -31,4 +31,5 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { truncate?: boolean; key?: string; closeCellPopover?: () => void; + enableActions?: boolean; }; From 9c5e96c56d7cc694e22ccff3c4a44401874329ea Mon Sep 17 00:00:00 2001 From: "Quynh Nguyen (Quinn)" <43350163+qn895@users.noreply.github.com> Date: Tue, 29 Nov 2022 10:19:30 -0600 Subject: [PATCH 08/11] [ML] Fix back button fails to return to ML from Discover after using View in Discover link from anomalies table (#146472) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> fixes https://github.com/elastic/kibana/issues/142305 Fixes https://github.com/elastic/kibana/issues/142305 --- .../application/components/anomalies_table/links_menu.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index fcc2a6abcba1f..efd07615e6d8d 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -253,11 +253,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const url = await discoverLocator.getRedirectUrl({ indexPatternId: dataViewId, - refreshInterval: { - display: 'Off', - pause: true, - value: 0, - }, timeRange: { from, to, @@ -271,7 +266,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { dataViewId === null ? [] : getFiltersForDSLQuery(job.datafeed_config.query, dataViewId, job.job_id), - sort: [['timestamp, asc']], }); if (!unmounted) { From e1227a3fe94bd37a0cc55481227a933714912003 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 29 Nov 2022 08:46:49 -0800 Subject: [PATCH 09/11] [DOCS] Add assignees query parameter to find cases API (#146087) --- .../cases/case-apis-passthru.asciidoc | 10 ++++++---- docs/api/cases/cases-api-find-cases.asciidoc | 6 ++++++ x-pack/plugins/cases/docs/openapi/bundled.json | 18 ++++++++++++++++++ x-pack/plugins/cases/docs/openapi/bundled.yaml | 9 +++++++++ .../paths/s@{spaceid}@api@cases@_find.yaml | 9 +++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/api-generated/cases/case-apis-passthru.asciidoc b/docs/api-generated/cases/case-apis-passthru.asciidoc index 16394ac263cee..80e8c7eb9cac7 100644 --- a/docs/api-generated/cases/case-apis-passthru.asciidoc +++ b/docs/api-generated/cases/case-apis-passthru.asciidoc @@ -1085,7 +1085,9 @@ Any modifications made to this file will be overwritten.

Query parameters

-
defaultSearchOperator (optional)
+
assignees (optional)
+ +
Query Parameter — Filters the returned cases by assignees. Valid values are none or unique identifiers for the user profiles. These identifiers can be found by using the suggest user profile API. default: null
defaultSearchOperator (optional)
Query Parameter — The default operator to use for the simple_query_string. default: OR
fields (optional)
@@ -1922,8 +1924,8 @@ Any modifications made to this file will be overwritten.
  • getCaseStatus_200_response -
  • getCasesByAlert_200_response_inner -
  • getCases_200_response -
  • +
  • getCases_assignees_parameter -
  • getCases_owner_parameter -
  • -
  • getCases_reporters_parameter -
  • owners -
  • payload_alert_comment -
  • payload_alert_comment_comment -
  • @@ -2485,13 +2487,13 @@ Any modifications made to this file will be overwritten.
    -

    getCases_reporters_parameter - Up

    +

    getCases_owner_parameter - Up

    diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc index ff79471a677fa..4f80086e8f8c4 100644 --- a/docs/api/cases/cases-api-find-cases.asciidoc +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -33,6 +33,12 @@ default space is used. === {api-query-parms-title} +`assignees`:: +(Optional, string or array of strings) Filters the returned cases by assignees. +Valid values are `none` or unique identifiers for the user profiles. These +identifiers can be found by using the +{ref}/security-api-suggest-user-profile.html[suggest user profile API]. + `defaultSearchOperator`:: (Optional, string) The default operator to use for the `simple_query_string`. Defaults to `OR`. diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index 532368176eefc..226d5578e8521 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -188,6 +188,24 @@ { "$ref": "#/components/parameters/space_id" }, + { + "name": "assignees", + "in": "query", + "description": "Filters the returned cases by assignees. Valid values are `none` or unique identifiers for the user profiles. These identifiers can be found by using the suggest user profile API.", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, { "name": "defaultSearchOperator", "in": "query", diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index 69959582c9224..e01bf8c4fea10 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -113,6 +113,15 @@ paths: - cases parameters: - $ref: '#/components/parameters/space_id' + - name: assignees + in: query + description: Filters the returned cases by assignees. Valid values are `none` or unique identifiers for the user profiles. These identifiers can be found by using the suggest user profile API. + schema: + oneOf: + - type: string + - type: array + items: + type: string - name: defaultSearchOperator in: query description: The default operator to use for the simple_query_string. diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@_find.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@_find.yaml index 89c8a0eb9876c..65fbc2e1e2e40 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@_find.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@_find.yaml @@ -9,6 +9,15 @@ get: - cases parameters: - $ref: '../components/parameters/space_id.yaml' + - name: assignees + in: query + description: Filters the returned cases by assignees. Valid values are `none` or unique identifiers for the user profiles. These identifiers can be found by using the suggest user profile API. + schema: + oneOf: + - type: string + - type: array + items: + type: string - name: defaultSearchOperator in: query description: The default operator to use for the simple_query_string. From fdcefb703d6aa366b040f24ee1a27eb14c7563c8 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 29 Nov 2022 11:47:52 -0500 Subject: [PATCH 10/11] feat(slo): Add optional settings (#146470) --- x-pack/plugins/observability/dev_docs/slo.md | 5 +++ .../slo_transform_template.ts | 16 +++++++--- .../server/domain/models/duration.test.ts | 23 +++++++++++++ .../server/domain/models/duration.ts | 4 +++ .../domain/services/validate_slo.test.ts | 25 +++++++++++++++ .../server/domain/services/validate_slo.ts | 32 +++++++++++++++++++ .../observability/server/saved_objects/slo.ts | 7 ++++ .../server/services/slo/create_slo.ts | 7 +++- .../server/services/slo/find_slo.test.ts | 5 +++ .../server/services/slo/fixtures/slo.ts | 7 ++++ .../server/services/slo/get_slo.test.ts | 6 +++- .../server/services/slo/get_slo.ts | 1 + .../server/services/slo/sli_client.ts | 6 +++- .../apm_transaction_duration.test.ts.snap | 2 +- .../apm_transaction_error_rate.test.ts.snap | 2 +- .../__snapshots__/kql_custom.test.ts.snap | 2 +- .../apm_transaction_duration.ts | 3 +- .../apm_transaction_error_rate.ts | 3 +- .../slo/transform_generators/kql_custom.ts | 3 +- .../transform_generator.ts | 12 ++++++- .../server/services/slo/update_slo.test.ts | 19 +++++++++++ .../server/services/slo/update_slo.ts | 5 +++ .../server/types/rest_specs/slo.ts | 25 ++++++++++----- .../observability/server/types/schema/slo.ts | 11 +++++++ 24 files changed, 209 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/observability/dev_docs/slo.md b/x-pack/plugins/observability/dev_docs/slo.md index ba9d58f8c772f..bb64afcf748a5 100644 --- a/x-pack/plugins/observability/dev_docs/slo.md +++ b/x-pack/plugins/observability/dev_docs/slo.md @@ -38,7 +38,12 @@ For example, defining a **timeslices** budgeting method with a `95%` slice thres The target objective is the value the SLO needs to meet during the time window. If a **timeslices** budgeting method is used, we also need to define the **timeslice_target** which can be different than the overall SLO target. +### Optional settings +The default settings should be sufficient for most users, but if needed, the following properties can be overwritten: +- timestamp_field: The date time field to use from the source index +- sync_delay: The ingest delay in the source data +- frequency: How often do we query the source data ## Example diff --git a/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts index 6b313bdb76c5a..d9a8bfb1de8cb 100644 --- a/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts +++ b/x-pack/plugins/observability/server/assets/transform_templates/slo_transform_template.ts @@ -10,26 +10,34 @@ import { TransformPivot, TransformPutTransformRequest, TransformSource, + TransformTimeSync, } from '@elastic/elasticsearch/lib/api/types'; +export interface TransformSettings { + frequency: TransformPutTransformRequest['frequency']; + sync_field: TransformTimeSync['field']; + sync_delay: TransformTimeSync['delay']; +} + export const getSLOTransformTemplate = ( transformId: string, source: TransformSource, destination: TransformDestination, groupBy: TransformPivot['group_by'] = {}, - aggregations: TransformPivot['aggregations'] = {} + aggregations: TransformPivot['aggregations'] = {}, + settings: TransformSettings ): TransformPutTransformRequest => ({ transform_id: transformId, source, - frequency: '1m', + frequency: settings.frequency, dest: destination, settings: { deduce_mappings: false, }, sync: { time: { - field: '@timestamp', - delay: '60s', + field: settings.sync_field, + delay: settings.sync_delay, }, }, pivot: { diff --git a/x-pack/plugins/observability/server/domain/models/duration.test.ts b/x-pack/plugins/observability/server/domain/models/duration.test.ts index 47c408766fea0..ce99d4be0571a 100644 --- a/x-pack/plugins/observability/server/domain/models/duration.test.ts +++ b/x-pack/plugins/observability/server/domain/models/duration.test.ts @@ -54,4 +54,27 @@ describe('Duration', () => { expect(long.isShorterThan(new Duration(1, DurationUnit.Year))).toBe(false); }); }); + + describe('isLongerOrEqualThan', () => { + it('returns true when the current duration is longer or equal than the other duration', () => { + const long = new Duration(2, DurationUnit.Year); + expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Hour))).toBe(true); + expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(true); + expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(true); + expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(true); + expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(true); + expect(long.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(true); + }); + + it('returns false when the current duration is shorter than the other duration', () => { + const short = new Duration(1, DurationUnit.Minute); + expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Minute))).toBe(true); + expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Hour))).toBe(false); + expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Day))).toBe(false); + expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Week))).toBe(false); + expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Month))).toBe(false); + expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Quarter))).toBe(false); + expect(short.isLongerOrEqualThan(new Duration(1, DurationUnit.Year))).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/observability/server/domain/models/duration.ts b/x-pack/plugins/observability/server/domain/models/duration.ts index f80482f38a5be..8662330c67465 100644 --- a/x-pack/plugins/observability/server/domain/models/duration.ts +++ b/x-pack/plugins/observability/server/domain/models/duration.ts @@ -34,6 +34,10 @@ class Duration { return currentDurationMoment.asSeconds() < otherDurationMoment.asSeconds(); } + isLongerOrEqualThan(other: Duration): boolean { + return !this.isShorterThan(other); + } + format(): string { return `${this.value}${this.unit}`; } diff --git a/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts b/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts index 0be391acf984f..ba246e7b5837f 100644 --- a/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts +++ b/x-pack/plugins/observability/server/domain/services/validate_slo.test.ts @@ -6,6 +6,7 @@ */ import { validateSLO } from '.'; +import { oneMinute, sixHours } from '../../services/slo/fixtures/duration'; import { createSLO } from '../../services/slo/fixtures/slo'; import { Duration, DurationUnit } from '../models/duration'; @@ -34,6 +35,30 @@ describe('validateSLO', () => { }); expect(() => validateSLO(slo)).toThrowError('Invalid time_window.duration'); }); + + describe('settings', () => { + it("throws when frequency is longer or equal than '1h'", () => { + const slo = createSLO({ + settings: { + frequency: sixHours(), + timestamp_field: '@timestamp', + sync_delay: oneMinute(), + }, + }); + expect(() => validateSLO(slo)).toThrowError('Invalid settings.frequency'); + }); + + it("throws when sync_delay is longer or equal than '6h'", () => { + const slo = createSLO({ + settings: { + frequency: oneMinute(), + timestamp_field: '@timestamp', + sync_delay: sixHours(), + }, + }); + expect(() => validateSLO(slo)).toThrowError('Invalid settings.sync_delay'); + }); + }); }); describe('slo with timeslices budgeting method', () => { diff --git a/x-pack/plugins/observability/server/domain/services/validate_slo.ts b/x-pack/plugins/observability/server/domain/services/validate_slo.ts index 8349ab1635290..e5994d7f09dab 100644 --- a/x-pack/plugins/observability/server/domain/services/validate_slo.ts +++ b/x-pack/plugins/observability/server/domain/services/validate_slo.ts @@ -41,6 +41,18 @@ export function validateSLO(slo: SLO) { throw new IllegalArgumentError('Invalid objective.timeslice_window'); } } + + validateSettings(slo); +} + +function validateSettings(slo: SLO) { + if (!isValidFrequencySettings(slo.settings.frequency)) { + throw new IllegalArgumentError('Invalid settings.frequency'); + } + + if (!isValidSyncDelaySettings(slo.settings.sync_delay)) { + throw new IllegalArgumentError('Invalid settings.sync_delay'); + } } function isValidTargetNumber(value: number): boolean { @@ -63,3 +75,23 @@ function isValidTimesliceWindowDuration(timesliceWindow: Duration, timeWindow: D timesliceWindow.isShorterThan(timeWindow) ); } + +/** + * validate that 1 minute <= frequency < 1 hour + */ +function isValidFrequencySettings(frequency: Duration): boolean { + return ( + frequency.isLongerOrEqualThan(new Duration(1, DurationUnit.Minute)) && + frequency.isShorterThan(new Duration(1, DurationUnit.Hour)) + ); +} + +/** + * validate that 1 minute <= sync_delay < 6 hour + */ +function isValidSyncDelaySettings(syncDelay: Duration): boolean { + return ( + syncDelay.isLongerOrEqualThan(new Duration(1, DurationUnit.Minute)) && + syncDelay.isShorterThan(new Duration(6, DurationUnit.Hour)) + ); +} diff --git a/x-pack/plugins/observability/server/saved_objects/slo.ts b/x-pack/plugins/observability/server/saved_objects/slo.ts index 5169c26252bc0..f63ef3881b9d5 100644 --- a/x-pack/plugins/observability/server/saved_objects/slo.ts +++ b/x-pack/plugins/observability/server/saved_objects/slo.ts @@ -46,6 +46,13 @@ export const slo: SavedObjectsType = { timeslice_window: { type: 'keyword' }, }, }, + settings: { + properties: { + timestamp_field: { type: 'keyword' }, + sync_delay: { type: 'keyword' }, + frequency: { type: 'keyword' }, + }, + }, revision: { type: 'short' }, created_at: { type: 'date' }, updated_at: { type: 'date' }, diff --git a/x-pack/plugins/observability/server/services/slo/create_slo.ts b/x-pack/plugins/observability/server/services/slo/create_slo.ts index aab2a42f7ede3..465eea0d04b71 100644 --- a/x-pack/plugins/observability/server/services/slo/create_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/create_slo.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; -import { SLO } from '../../domain/models'; +import { Duration, DurationUnit, SLO } from '../../domain/models'; import { ResourceInstaller } from './resource_installer'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; @@ -55,6 +55,11 @@ export class CreateSLO { return { ...params, id: uuid.v1(), + settings: { + timestamp_field: params.settings?.timestamp_field ?? '@timestamp', + sync_delay: params.settings?.sync_delay ?? new Duration(1, DurationUnit.Minute), + frequency: params.settings?.frequency ?? new Duration(1, DurationUnit.Minute), + }, revision: 1, created_at: now, updated_at: now, diff --git a/x-pack/plugins/observability/server/services/slo/find_slo.test.ts b/x-pack/plugins/observability/server/services/slo/find_slo.test.ts index 5de0afb3aa3d7..1ac802183c22a 100644 --- a/x-pack/plugins/observability/server/services/slo/find_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/find_slo.test.ts @@ -64,6 +64,11 @@ describe('FindSLO', () => { duration: '7d', is_rolling: true, }, + settings: { + timestamp_field: '@timestamp', + sync_delay: '1m', + frequency: '1m', + }, summary: { sli_value: 0.9999, error_budget: { diff --git a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts index 2d0b24b77959a..37c93e8cdfc0e 100644 --- a/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts +++ b/x-pack/plugins/observability/server/services/slo/fixtures/slo.ts @@ -14,6 +14,8 @@ import { sloSchema } from '../../../types/schema'; import { APMTransactionDurationIndicator, APMTransactionErrorRateIndicator, + Duration, + DurationUnit, Indicator, KQLCustomIndicator, SLO, @@ -74,6 +76,11 @@ const defaultSLO: Omit = { target: 0.999, }, indicator: createAPMTransactionDurationIndicator(), + settings: { + timestamp_field: '@timestamp', + sync_delay: new Duration(1, DurationUnit.Minute), + frequency: new Duration(1, DurationUnit.Minute), + }, }; export const createSLOParams = (params: Partial = {}): CreateSLOParams => ({ diff --git a/x-pack/plugins/observability/server/services/slo/get_slo.test.ts b/x-pack/plugins/observability/server/services/slo/get_slo.test.ts index 49730de0e7424..906edb75664a7 100644 --- a/x-pack/plugins/observability/server/services/slo/get_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/get_slo.test.ts @@ -60,7 +60,11 @@ describe('GetSLO', () => { duration: '7d', is_rolling: true, }, - + settings: { + timestamp_field: '@timestamp', + sync_delay: '1m', + frequency: '1m', + }, summary: { sli_value: 0.9999, error_budget: { diff --git a/x-pack/plugins/observability/server/services/slo/get_slo.ts b/x-pack/plugins/observability/server/services/slo/get_slo.ts index 802ba5499ed86..62b2e7b9cd876 100644 --- a/x-pack/plugins/observability/server/services/slo/get_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/get_slo.ts @@ -32,6 +32,7 @@ export class GetSLO { time_window: slo.time_window, budgeting_method: slo.budgeting_method, objective: slo.objective, + settings: slo.settings, summary: slo.summary, revision: slo.revision, created_at: slo.created_at, diff --git a/x-pack/plugins/observability/server/services/slo/sli_client.ts b/x-pack/plugins/observability/server/services/slo/sli_client.ts index a62ba6c6817c6..11294b1b82229 100644 --- a/x-pack/plugins/observability/server/services/slo/sli_client.ts +++ b/x-pack/plugins/observability/server/services/slo/sli_client.ts @@ -55,11 +55,15 @@ export class DefaultSLIClient implements SLIClient { generateSearchQuery(slo, dateRangeBySlo[slo.id]), ]); + const indicatorDataBySlo: Record = {}; + if (searches.length === 0) { + return indicatorDataBySlo; + } + const result = await this.esClient.msearch>({ searches, }); - const indicatorDataBySlo: Record = {}; for (let i = 0; i < result.responses.length; i++) { const slo = sloList[i]; if ('error' in result.responses[i]) { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap index 187126b8e8efd..e62dab5e112e2 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap @@ -177,7 +177,7 @@ Object { }, "sync": Object { "time": Object { - "delay": "60s", + "delay": "1m", "field": "@timestamp", }, }, diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap index 3a7826cb9d8b5..0d41f6a45fb8a 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -182,7 +182,7 @@ Object { }, "sync": Object { "time": Object { - "delay": "60s", + "delay": "1m", "field": "@timestamp", }, }, diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap index 5fed8a8e08977..1aa05dfb2cf3b 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/kql_custom.test.ts.snap @@ -242,7 +242,7 @@ Object { }, "sync": Object { "time": Object { - "delay": "60s", + "delay": "1m", "field": "@timestamp", }, }, diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts index 2725c33af3940..6cba9c32d40ac 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts @@ -30,7 +30,8 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator this.buildSource(slo, slo.indicator), this.buildDestination(), this.buildCommonGroupBy(slo), - this.buildAggregations(slo.indicator) + this.buildAggregations(slo.indicator), + this.buildSettings(slo) ); } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts index b0418d5f1f9f3..a68eb490ebde1 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts @@ -32,7 +32,8 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato this.buildSource(slo, slo.indicator), this.buildDestination(), this.buildCommonGroupBy(slo), - this.buildAggregations(slo, slo.indicator) + this.buildAggregations(slo, slo.indicator), + this.buildSettings(slo) ); } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts index 02809a0d05659..f5497118ef611 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/kql_custom.ts @@ -30,7 +30,8 @@ export class KQLCustomTransformGenerator extends TransformGenerator { this.buildSource(slo, slo.indicator), this.buildDestination(), this.buildCommonGroupBy(slo), - this.buildAggregations(slo, slo.indicator) + this.buildAggregations(slo, slo.indicator), + this.buildSettings(slo) ); } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts index 69a80fa9a0f4e..2903d2791d6b7 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts @@ -10,6 +10,7 @@ import { AggregationsCalendarInterval, TransformPutTransformRequest, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TransformSettings } from '../../../assets/transform_templates/slo_transform_template'; import { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema, @@ -140,12 +141,21 @@ export abstract class TransformGenerator { }, }, }), + // Field used in the destination index, using @timestamp as per mapping definition '@timestamp': { date_histogram: { - field: '@timestamp', + field: slo.settings.timestamp_field, calendar_interval: '1m' as AggregationsCalendarInterval, }, }, }; } + + public buildSettings(slo: SLO): TransformSettings { + return { + frequency: slo.settings.frequency.format(), + sync_field: slo.settings.timestamp_field, + sync_delay: slo.settings.sync_delay.format(), + }; + } } diff --git a/x-pack/plugins/observability/server/services/slo/update_slo.test.ts b/x-pack/plugins/observability/server/services/slo/update_slo.test.ts index cd3dbb6352936..db0e726c83066 100644 --- a/x-pack/plugins/observability/server/services/slo/update_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/update_slo.test.ts @@ -47,6 +47,25 @@ describe('UpdateSLO', () => { }); describe('with breaking changes', () => { + it('consideres settings as a breaking change', async () => { + const slo = createSLO(); + mockRepository.findById.mockResolvedValueOnce(slo); + + const newSettings = { ...slo.settings, timestamp_field: 'newField' }; + await updateSLO.execute(slo.id, { settings: newSettings }); + + expectDeletionOfObsoleteSLOData(slo); + expect(mockRepository.save).toBeCalledWith( + expect.objectContaining({ + ...slo, + settings: newSettings, + revision: 2, + updated_at: expect.anything(), + }) + ); + expectInstallationOfNewSLOTransform(); + }); + it('removes the obsolete data from the SLO previous revision', async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ environment: 'development' }), diff --git a/x-pack/plugins/observability/server/services/slo/update_slo.ts b/x-pack/plugins/observability/server/services/slo/update_slo.ts index 8712119d345de..0a496a177b213 100644 --- a/x-pack/plugins/observability/server/services/slo/update_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/update_slo.ts @@ -52,6 +52,10 @@ export class UpdateSLO { hasBreakingChange = true; } + if (!deepEqual(originalSlo.settings, updatedSlo.settings)) { + hasBreakingChange = true; + } + if (hasBreakingChange) { updatedSlo.revision++; } @@ -87,6 +91,7 @@ export class UpdateSLO { budgeting_method: slo.budgeting_method, time_window: slo.time_window, objective: slo.objective, + settings: slo.settings, created_at: slo.created_at, updated_at: slo.updated_at, }); diff --git a/x-pack/plugins/observability/server/types/rest_specs/slo.ts b/x-pack/plugins/observability/server/types/rest_specs/slo.ts index 2dd83528b2573..40253ad998b12 100644 --- a/x-pack/plugins/observability/server/types/rest_specs/slo.ts +++ b/x-pack/plugins/observability/server/types/rest_specs/slo.ts @@ -12,19 +12,24 @@ import { dateType, indicatorSchema, objectiveSchema, + optionalSettingsSchema, + settingsSchema, summarySchema, timeWindowSchema, } from '../schema'; const createSLOParamsSchema = t.type({ - body: t.type({ - name: t.string, - description: t.string, - indicator: indicatorSchema, - time_window: timeWindowSchema, - budgeting_method: budgetingMethodSchema, - objective: objectiveSchema, - }), + body: t.intersection([ + t.type({ + name: t.string, + description: t.string, + indicator: indicatorSchema, + time_window: timeWindowSchema, + budgeting_method: budgetingMethodSchema, + objective: objectiveSchema, + }), + t.partial({ settings: optionalSettingsSchema }), + ]), }); const createSLOResponseSchema = t.type({ @@ -59,6 +64,7 @@ const getSLOResponseSchema = t.type({ time_window: timeWindowSchema, budgeting_method: budgetingMethodSchema, objective: objectiveSchema, + settings: settingsSchema, summary: summarySchema, revision: t.number, created_at: dateType, @@ -76,6 +82,7 @@ const updateSLOParamsSchema = t.type({ time_window: timeWindowSchema, budgeting_method: budgetingMethodSchema, objective: objectiveSchema, + settings: settingsSchema, }), }); @@ -87,6 +94,7 @@ const updateSLOResponseSchema = t.type({ time_window: timeWindowSchema, budgeting_method: budgetingMethodSchema, objective: objectiveSchema, + settings: settingsSchema, created_at: dateType, updated_at: dateType, }); @@ -105,6 +113,7 @@ const findSLOResponseSchema = t.type({ budgeting_method: budgetingMethodSchema, objective: objectiveSchema, summary: summarySchema, + settings: settingsSchema, revision: t.number, created_at: dateType, updated_at: dateType, diff --git a/x-pack/plugins/observability/server/types/schema/slo.ts b/x-pack/plugins/observability/server/types/schema/slo.ts index a2e2145af5e8c..99aa630303868 100644 --- a/x-pack/plugins/observability/server/types/schema/slo.ts +++ b/x-pack/plugins/observability/server/types/schema/slo.ts @@ -24,6 +24,14 @@ const objectiveSchema = t.intersection([ t.partial({ timeslice_target: t.number, timeslice_window: durationType }), ]); +const settingsSchema = t.type({ + timestamp_field: t.string, + sync_delay: durationType, + frequency: durationType, +}); + +const optionalSettingsSchema = t.partial({ ...settingsSchema.props }); + const sloSchema = t.type({ id: t.string, name: t.string, @@ -32,6 +40,7 @@ const sloSchema = t.type({ time_window: timeWindowSchema, budgeting_method: budgetingMethodSchema, objective: objectiveSchema, + settings: settingsSchema, revision: t.number, created_at: dateType, updated_at: dateType, @@ -43,6 +52,8 @@ export { budgetingMethodSchema, objectiveSchema, occurrencesBudgetingMethodSchema, + optionalSettingsSchema, + settingsSchema, sloSchema, sloWithSummarySchema, timeslicesBudgetingMethodSchema, From 36808ec3d3a370abb61f5469efa6f3c9cdc328a1 Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Tue, 29 Nov 2022 17:50:22 +0100 Subject: [PATCH 11/11] [APM] Fix wide button sizes in ml callout (#146571) ## Summary closes: https://github.com/elastic/kibana/issues/145870 Before ![image](https://user-images.githubusercontent.com/3369346/204577198-def490fe-2594-4455-a180-37eec5f73715.png) After image --- .../apm/public/components/shared/ml_callout/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx b/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx index 7ae0440bbef3a..12b4e021e2302 100644 --- a/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ml_callout/index.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -167,7 +167,7 @@ export function MLCallout({ const hasAnyActions = properties.primaryAction || dismissable; const actions = hasAnyActions ? ( - + {properties.primaryAction && ( {properties.primaryAction} )} @@ -180,7 +180,7 @@ export function MLCallout({ )} - + ) : null; return (