diff --git a/packages/elastic-apm-synthtrace/src/index.ts b/packages/elastic-apm-synthtrace/src/index.ts index ab6a3e3731be70..3e7a2f1d591906 100644 --- a/packages/elastic-apm-synthtrace/src/index.ts +++ b/packages/elastic-apm-synthtrace/src/index.ts @@ -9,6 +9,7 @@ export { timerange } from './lib/timerange'; export { apm } from './lib/apm'; export { stackMonitoring } from './lib/stack_monitoring'; +export { observer } from './lib/agent_config'; export { cleanWriteTargets } from './lib/utils/clean_write_targets'; export { createLogger, LogLevel } from './lib/utils/create_logger'; diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config.ts new file mode 100644 index 00000000000000..5ec90035141dab --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AgentConfigFields } from './agent_config_fields'; +import { Metricset } from '../apm/metricset'; + +export class AgentConfig extends Metricset { + constructor() { + super({ + 'metricset.name': 'agent_config', + agent_config_applied: 1, + }); + } + + etag(etag: string) { + this.fields['labels.etag'] = etag; + return this; + } +} diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config_fields.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config_fields.ts new file mode 100644 index 00000000000000..82b0963cee6e69 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/agent_config_fields.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ApmFields } from '../apm/apm_fields'; + +export type AgentConfigFields = Pick< + ApmFields, + | '@timestamp' + | 'processor.event' + | 'processor.name' + | 'metricset.name' + | 'observer' + | 'ecs.version' + | 'event.ingested' +> & + Partial<{ + 'labels.etag': string; + agent_config_applied: number; + 'event.agent_id_status': string; + }>; diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts new file mode 100644 index 00000000000000..204a12386b275a --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { observer } from './observer'; diff --git a/packages/elastic-apm-synthtrace/src/lib/agent_config/observer.ts b/packages/elastic-apm-synthtrace/src/lib/agent_config/observer.ts new file mode 100644 index 00000000000000..189f3f62abb39d --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/agent_config/observer.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AgentConfigFields } from './agent_config_fields'; +import { AgentConfig } from './agent_config'; +import { Entity } from '../entity'; + +export class Observer extends Entity { + agentConfig() { + return new AgentConfig(); + } +} + +export function observer() { + return new Observer({}); +} diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts b/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts index 4051d7e8241da3..9a7664e9518ced 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/instance.ts @@ -45,7 +45,7 @@ export class Instance extends Entity { } appMetrics(metrics: ApmApplicationMetricFields) { - return new Metricset({ + return new Metricset({ ...this.fields, 'metricset.name': 'app', ...metrics, diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts b/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts index 88177e816a8521..515af829c6a5af 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/metricset.ts @@ -7,10 +7,10 @@ */ import { Serializable } from '../serializable'; -import { ApmFields } from './apm_fields'; +import { Fields } from '../entity'; -export class Metricset extends Serializable { - constructor(fields: ApmFields) { +export class Metricset extends Serializable { + constructor(fields: TFields) { super({ 'processor.event': 'metric', 'processor.name': 'metric', diff --git a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts index e1cb332996e236..a6f8f923b3714d 100644 --- a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts +++ b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts @@ -211,7 +211,9 @@ export class StreamProcessor { const eventType = d.processor.event as keyof ApmElasticsearchOutputWriteTargets; let dataStream = writeTargets[eventType]; if (eventType === 'metric') { - if (!d.service?.name) { + if (d.metricset?.name === 'agent_config') { + dataStream = 'metrics-apm.internal-default'; + } else if (!d.service?.name) { dataStream = 'metrics-apm.app-default'; } else { if (!d.transaction && !d.span) { diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/04_agent_config.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/04_agent_config.ts new file mode 100644 index 00000000000000..ec6d57eba4b615 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/04_agent_config.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { observer, timerange } from '../..'; +import { Scenario } from '../scenario'; +import { getLogger } from '../utils/get_common_services'; +import { RunOptions } from '../utils/parse_run_cli_flags'; +import { AgentConfigFields } from '../../lib/agent_config/agent_config_fields'; + +const scenario: Scenario = async (runOptions: RunOptions) => { + const logger = getLogger(runOptions); + + return { + generate: ({ from, to }) => { + const agentConfig = observer().agentConfig(); + + const range = timerange(from, to); + return range + .interval('30s') + .rate(1) + .generator((timestamp) => { + const events = logger.perf('generating_agent_config_events', () => { + return agentConfig.etag('test-etag').timestamp(timestamp); + }); + return events; + }); + }, + }; +}; + +export default scenario; diff --git a/src/plugins/unified_search/public/actions/apply_filter_action.ts b/src/plugins/unified_search/public/actions/apply_filter_action.ts index 36524cf3ff8263..465d6d33890dea 100644 --- a/src/plugins/unified_search/public/actions/apply_filter_action.ts +++ b/src/plugins/unified_search/public/actions/apply_filter_action.ts @@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n'; import { ThemeServiceSetup } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { Action, createAction, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import { Filter, FilterManager, TimefilterContract, esFilters } from '@kbn/data-plugin/public'; +// for cleanup esFilters need to fix the issue https://github.com/elastic/kibana/issues/131292 +import { FilterManager, TimefilterContract, esFilters } from '@kbn/data-plugin/public'; +import type { Filter } from '@kbn/es-query'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../apply_filters'; diff --git a/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx b/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx index 9017fbf40ee2f6..8119127e87e2ca 100644 --- a/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx @@ -24,7 +24,7 @@ import { mapAndFlattenFilters, getFieldDisplayValueFromFilter, } from '@kbn/data-plugin/public'; -import { Filter } from '@kbn/data-plugin/common'; +import type { Filter } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/public'; import { FilterLabel } from '../filter_bar'; diff --git a/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx b/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx index 4cefbd1a202a00..8c515ae4e6d785 100644 --- a/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx +++ b/src/plugins/unified_search/public/apply_filters/apply_filters_popover.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { Filter } from '@kbn/data-plugin/common'; +import type { Filter } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/common'; type CancelFnType = () => void; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts index 24a27bcb99fbe1..d5535383298743 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts @@ -7,7 +7,7 @@ */ import { coreMock } from '@kbn/core/public/mocks'; -import { KueryNode } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { setupGetConjunctionSuggestions } from './conjunction'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts index 4446fcf685bdee..085ba3dc0979f1 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts @@ -8,7 +8,8 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; -import { indexPatterns as indexPatternsUtils, KueryNode } from '@kbn/data-plugin/public'; +import { indexPatterns as indexPatternsUtils } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { setupGetFieldSuggestions } from './field'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx index 723b7e6896229a..37f9c4658b81a2 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// for replace IFieldType => DataViewField need to fix the issue https://github.com/elastic/kibana/issues/131292 import { IFieldType, indexPatterns as indexPatternsUtils } from '@kbn/data-plugin/public'; import { flatten } from 'lodash'; import { sortPrefixFirst } from './sort_prefix_first'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts index a40678ad4ac165..7e2340fdb043a0 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts @@ -9,7 +9,7 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { setupGetOperatorSuggestions } from './operator'; -import { KueryNode } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts index 3405d26824a263..e852e8e11f3473 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts @@ -10,7 +10,7 @@ import { setupGetValueSuggestions } from './value'; import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { coreMock } from '@kbn/core/public/mocks'; -import { KueryNode } from '@kbn/data-plugin/public'; +import type { KueryNode } from '@kbn/es-query'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; const mockKueryNode = (kueryNode: Partial) => kueryNode as unknown as KueryNode; diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts index 06b0fc9639a3cf..0bbf416d99a2e5 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts @@ -8,7 +8,9 @@ import { flatten } from 'lodash'; import { CoreSetup } from '@kbn/core/public'; -import { IFieldType, IIndexPattern } from '@kbn/data-plugin/public'; +// for replace IIndexPattern => DataView and IFieldType => DataViewField +// need to fix the issue https://github.com/elastic/kibana/issues/131292 +import type { IIndexPattern, IFieldType } from '@kbn/data-views-plugin/common'; import { escapeQuotes } from './lib/escape_kuery'; import { KqlQuerySuggestionProvider } from './types'; import type { UnifiedSearchPublicPluginStart } from '../../../types'; diff --git a/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts index 056fcb716054a8..2e0e5c793f82fd 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts @@ -7,7 +7,8 @@ */ import { ValueSuggestionsMethod } from '@kbn/data-plugin/common'; -import { IFieldType, IIndexPattern } from '@kbn/data-plugin/common'; +// for replace IIndexPattern => DataView need to fix the issue https://github.com/elastic/kibana/issues/131292 +import type { DataViewField, IIndexPattern } from '@kbn/data-views-plugin/common'; export enum QuerySuggestionTypes { Field = 'field', @@ -47,7 +48,7 @@ export interface QuerySuggestionBasic { /** @public **/ export interface QuerySuggestionField extends QuerySuggestionBasic { type: QuerySuggestionTypes.Field; - field: IFieldType; + field: DataViewField; } /** @public **/ diff --git a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts index 2c25fe02305011..8d08a9de2577de 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts @@ -9,12 +9,10 @@ import { CoreSetup } from '@kbn/core/public'; import dateMath from '@kbn/datemath'; import { memoize } from 'lodash'; -import { - IIndexPattern, - IFieldType, - UI_SETTINGS, - ValueSuggestionsMethod, -} from '@kbn/data-plugin/common'; +import { UI_SETTINGS, ValueSuggestionsMethod } from '@kbn/data-plugin/common'; +// for replace IIndexPattern => DataView and IFieldType => DataViewField +// need to fix the issue https://github.com/elastic/kibana/issues/131292 +import type { IIndexPattern, IFieldType } from '@kbn/data-views-plugin/common'; import type { TimefilterSetup } from '@kbn/data-plugin/public'; import { AutocompleteUsageCollector } from '../collectors'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx index 490d6480b28c95..6a3d7192ab9053 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx @@ -33,7 +33,7 @@ import { import { get } from 'lodash'; import React, { Component } from 'react'; import { XJsonLang } from '@kbn/monaco'; -import { DataView, IFieldType } from '@kbn/data-views-plugin/common'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { getIndexPatternFromFilter } from '@kbn/data-plugin/public'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; @@ -61,7 +61,7 @@ export interface Props { interface State { selectedIndexPattern?: DataView; - selectedField?: IFieldType; + selectedField?: DataViewField; selectedOperator?: Operator; params: any; useCustomLabel: boolean; @@ -447,7 +447,7 @@ class FilterEditorUI extends Component { this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); }; - private onFieldChange = ([selectedField]: IFieldType[]) => { + private onFieldChange = ([selectedField]: DataViewField[]) => { const selectedOperator = undefined; const params = undefined; this.setState({ selectedField, selectedOperator, params }); @@ -529,7 +529,7 @@ function IndexPatternComboBox(props: GenericComboBoxProps) { return GenericComboBox(props); } -function FieldComboBox(props: GenericComboBoxProps) { +function FieldComboBox(props: GenericComboBoxProps) { return GenericComboBox(props); } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index d6c44228eb72fa..07ce05d0395828 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -14,7 +14,7 @@ import { stubIndexPattern, stubFields, } from '@kbn/data-plugin/common/stubs'; -import { toggleFilterNegated } from '@kbn/data-plugin/common'; +import { toggleFilterNegated } from '@kbn/es-query'; import { getFieldFromFilter, getFilterableFields, diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts index f85b9a9e788d88..0863d10fe0c104 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -10,8 +10,8 @@ import dateMath from '@kbn/datemath'; import { Filter, FieldFilter } from '@kbn/es-query'; import { ES_FIELD_TYPES } from '@kbn/field-types'; import isSemverValid from 'semver/functions/valid'; -import { isFilterable, IFieldType, IpAddress } from '@kbn/data-plugin/common'; -import { DataView } from '@kbn/data-views-plugin/common'; +import { isFilterable, IpAddress } from '@kbn/data-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { FILTER_OPERATORS, Operator } from './filter_operators'; export function getFieldFromFilter(filter: FieldFilter, indexPattern: DataView) { @@ -28,7 +28,7 @@ export function getFilterableFields(indexPattern: DataView) { return indexPattern.fields.filter(isFilterable); } -export function getOperatorOptions(field: IFieldType) { +export function getOperatorOptions(field: DataViewField) { return FILTER_OPERATORS.filter((operator) => { if (operator.field) return operator.field(field); if (operator.fieldTypes) return operator.fieldTypes.includes(field.type); @@ -36,7 +36,7 @@ export function getOperatorOptions(field: IFieldType) { }); } -export function validateParams(params: any, field: IFieldType) { +export function validateParams(params: any, field: DataViewField) { switch (field.type) { case 'date': const moment = typeof params === 'string' ? dateMath.parse(params) : null; @@ -59,7 +59,7 @@ export function validateParams(params: any, field: IFieldType) { export function isFilterValid( indexPattern?: DataView, - field?: IFieldType, + field?: DataViewField, operator?: Operator, params?: any ) { diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx index 601cf68141c499..35c05316465f80 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx @@ -9,7 +9,7 @@ import React, { Fragment } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Filter, FILTERS } from '@kbn/data-plugin/common'; +import { Filter, FILTERS } from '@kbn/es-query'; import { existsOperator, isOneOfOperator } from './filter_operators'; import type { FilterLabelStatus } from '../../filter_item/filter_item'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts index c1e4d5361e3f80..6143158d69d5c7 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FILTERS } from '@kbn/es-query'; import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { IFieldType } from '@kbn/data-views-plugin/common'; +import { DataViewField } from '@kbn/data-views-plugin/common'; export interface Operator { message: string; @@ -25,7 +25,7 @@ export interface Operator { * A filter predicate for a field, * takes precedence over {@link fieldTypes} */ - field?: (field: IFieldType) => boolean; + field?: (field: DataViewField) => boolean; } export const isOperator = { @@ -68,7 +68,7 @@ export const isBetweenOperator = { }), type: FILTERS.RANGE, negate: false, - field: (field: IFieldType) => { + field: (field: DataViewField) => { if (['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'].includes(field.type)) return true; @@ -84,7 +84,7 @@ export const isNotBetweenOperator = { }), type: FILTERS.RANGE, negate: true, - field: (field: IFieldType) => { + field: (field: DataViewField) => { if (['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'].includes(field.type)) return true; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx index 50acadea2a9907..dc987421e26616 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; -import { IFieldType, UI_SETTINGS } from '@kbn/data-plugin/common'; -import { DataView } from '@kbn/data-views-plugin/common'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { IDataPluginServices } from '@kbn/data-plugin/public'; import { debounce } from 'lodash'; @@ -18,7 +18,7 @@ import { getAutocomplete } from '../../services'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; indexPattern: DataView; - field: IFieldType; + field: DataViewField; timeRangeForSuggestionsOverride?: boolean; } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx index 3c1046d9289814..26a25886ac8666 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx @@ -12,7 +12,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { get } from 'lodash'; import React from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { IFieldType } from '@kbn/data-plugin/common'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { ValueInputType } from './value_input_type'; interface RangeParams { @@ -23,7 +23,7 @@ interface RangeParams { type RangeParamsPartial = Partial; interface Props { - field: IFieldType; + field: DataViewField; value?: RangeParams; onChange: (params: RangeParamsPartial) => void; intl: InjectedIntl; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx index 1e50e92cec7bb7..a87888ed85c93b 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx @@ -10,12 +10,12 @@ import { EuiFieldNumber, EuiFieldText, EuiSelect } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { isEmpty } from 'lodash'; import React, { Component } from 'react'; -import { IFieldType } from '@kbn/data-views-plugin/common'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { validateParams } from './lib/filter_editor_utils'; interface Props { value?: string | number; - field: IFieldType; + field: DataViewField; onChange: (value: string | number | boolean) => void; onBlur?: (value: string | number | boolean) => void; placeholder: string; diff --git a/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx index 04b15aac847783..4dc7dc0f3b57b3 100644 --- a/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/create_index_pattern_select.tsx @@ -8,11 +8,11 @@ import React from 'react'; -import { IndexPatternsContract } from '@kbn/data-plugin/public'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { IndexPatternSelect, IndexPatternSelectProps } from '.'; // Takes in stateful runtime dependencies and pre-wires them to the component -export function createIndexPatternSelect(indexPatternService: IndexPatternsContract) { +export function createIndexPatternSelect(indexPatternService: DataViewsContract) { return (props: IndexPatternSelectProps) => ( ); diff --git a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx index 335787d2ee38ab..81534575d10b18 100644 --- a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx @@ -11,7 +11,7 @@ import React, { Component } from 'react'; import { Required } from '@kbn/utility-types'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; -import { IndexPatternsContract } from '@kbn/data-plugin/public'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; export type IndexPatternSelectProps = Required< Omit< @@ -26,7 +26,7 @@ export type IndexPatternSelectProps = Required< }; export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { - indexPatternService: IndexPatternsContract; + indexPatternService: DataViewsContract; }; interface IndexPatternSelectState { diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index c73aa258863ed3..a90098ebcf1564 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -12,7 +12,8 @@ import { CoreStart } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { QueryStart, SavedQuery, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { Filter, Query, TimeRange } from '@kbn/data-plugin/common'; +import { Query, TimeRange } from '@kbn/data-plugin/common'; +import type { Filter } from '@kbn/es-query'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { SearchBar } from '.'; import type { SearchBarOwnProps } from '.'; diff --git a/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts b/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts index 511e05e043b263..a6d0487cb90c71 100644 --- a/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts +++ b/src/plugins/unified_search/public/search_bar/lib/use_filter_manager.ts @@ -8,7 +8,8 @@ import { useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; -import { DataPublicPluginStart, Filter } from '@kbn/data-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { Filter } from '@kbn/es-query'; interface UseFilterManagerProps { filters?: Filter[]; diff --git a/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts b/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts index 2954526d7ede8d..10444d1d190553 100644 --- a/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts +++ b/src/plugins/unified_search/public/test_helpers/get_stub_filter.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { Filter, FilterStateStore } from '@kbn/data-plugin/public'; +import { FilterStateStore } from '@kbn/data-plugin/public'; +import type { Filter } from '@kbn/es-query'; export function getFilter( store: FilterStateStore, diff --git a/src/plugins/unified_search/public/utils/helpers.test.ts b/src/plugins/unified_search/public/utils/helpers.test.ts index 4659e356022283..803d6c53bb0076 100644 --- a/src/plugins/unified_search/public/utils/helpers.test.ts +++ b/src/plugins/unified_search/public/utils/helpers.test.ts @@ -7,11 +7,11 @@ */ import { getFieldValidityAndErrorMessage } from './helpers'; -import { IFieldType } from '@kbn/data-views-plugin/common'; +import { DataViewField } from '@kbn/data-views-plugin/common'; const mockField = { type: 'date', -} as IFieldType; +} as DataViewField; describe('Check field validity and error message', () => { it('should return a message that the entered date is not incorrect', () => { diff --git a/src/plugins/unified_search/public/utils/helpers.ts b/src/plugins/unified_search/public/utils/helpers.ts index 1c056636c67b81..6f0a605fa0e142 100644 --- a/src/plugins/unified_search/public/utils/helpers.ts +++ b/src/plugins/unified_search/public/utils/helpers.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import type { IFieldType } from '@kbn/data-views-plugin/common'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; import { isEmpty } from 'lodash'; import { validateParams } from '../filter_bar/filter_editor/lib/filter_editor_utils'; export const getFieldValidityAndErrorMessage = ( - field: IFieldType, + field: DataViewField, value?: string | undefined ): { isInvalid: boolean; errorMessage?: string } => { const type = field.type; diff --git a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts index f27fa9d594f974..03ceffc73b34f4 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts @@ -10,6 +10,7 @@ import { coreMock } from '@kbn/core/server/mocks'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { ConfigSchema } from '../../config'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { termsAggSuggestions } from './terms_agg'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { duration } from 'moment'; @@ -22,6 +23,8 @@ const configMock = { }, } as unknown as ConfigSchema; +const dataViewFieldMock = { name: 'field_name', type: 'string' } as DataViewField; + // @ts-expect-error not full interface const mockResponse = { aggregations: { @@ -50,7 +53,7 @@ describe('terms agg suggestions', () => { 'fieldName', 'query', [], - { name: 'field_name', type: 'string' } + dataViewFieldMock ); const [[args]] = esClientMock.search.mock.calls; diff --git a/src/plugins/unified_search/server/autocomplete/terms_agg.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.ts index ffdaca8caad4bc..c7d303e526ca8a 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_agg.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.ts @@ -9,7 +9,8 @@ import { get, map } from 'lodash'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IFieldType, getFieldSubtypeNested } from '@kbn/data-plugin/common'; +import { getFieldSubtypeNested } from '@kbn/data-plugin/common'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; import { ConfigSchema } from '../../config'; import { findIndexPatternById, getFieldByName } from '../data_views'; @@ -21,7 +22,7 @@ export async function termsAggSuggestions( fieldName: string, query: string, filters?: estypes.QueryDslQueryContainer[], - field?: IFieldType, + field?: FieldSpec, abortSignal?: AbortSignal ) { const autocompleteSearchOptions = { @@ -54,11 +55,11 @@ export async function termsAggSuggestions( async function getBody( // eslint-disable-next-line @typescript-eslint/naming-convention { timeout, terminate_after }: Record, - field: IFieldType | string, + field: FieldSpec | string, query: string, filters: estypes.QueryDslQueryContainer[] = [] ) { - const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); + const isFieldObject = (f: any): f is FieldSpec => Boolean(f && f.name); // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators const getEscapedQuery = (q: string = '') => diff --git a/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts b/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts index bc2a4e010a7657..f0209e66ee58d5 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts @@ -12,12 +12,19 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/serve import { ConfigSchema } from '../../config'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { TermsEnumResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; let savedObjectsClientMock: jest.Mocked; let esClientMock: DeeplyMockedKeys; const configMock = { autocomplete: { valueSuggestions: { tiers: ['data_hot', 'data_warm', 'data_content'] } }, } as ConfigSchema; +const dataViewFieldMock = { + name: 'field_name', + type: 'string', + searchable: true, + aggregatable: true, +} as DataViewField; const mockResponse = { terms: ['whoa', 'amazing'] }; jest.mock('../data_views'); @@ -39,7 +46,7 @@ describe('_terms_enum suggestions', () => { 'fieldName', 'query', [], - { name: 'field_name', type: 'string', searchable: true, aggregatable: true } + dataViewFieldMock ); const [[args]] = esClientMock.termsEnum.mock.calls; diff --git a/src/plugins/unified_search/server/autocomplete/terms_enum.ts b/src/plugins/unified_search/server/autocomplete/terms_enum.ts index 924b5b3a1671ec..3e8207eb644e5b 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_enum.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_enum.ts @@ -8,7 +8,7 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IFieldType } from '@kbn/data-plugin/common'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; import { findIndexPatternById, getFieldByName } from '../data_views'; import { ConfigSchema } from '../../config'; @@ -20,7 +20,7 @@ export async function termsEnumSuggestions( fieldName: string, query: string, filters?: estypes.QueryDslQueryContainer[], - field?: IFieldType, + field?: FieldSpec, abortSignal?: AbortSignal ) { const { tiers } = config.autocomplete.valueSuggestions; diff --git a/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts b/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts index 0f315c1583f1a3..88302dea912005 100644 --- a/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts +++ b/x-pack/plugins/apm/common/agent_configuration/configuration_types.d.ts @@ -15,6 +15,6 @@ export type AgentConfigurationIntake = t.TypeOf< export type AgentConfiguration = { '@timestamp': number; applied_by_agent?: boolean; - etag?: string; + etag: string; agent_name?: string; } & AgentConfigurationIntake; diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts index d52b048bc6b465..a0b3fa2e45c54d 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/convert_settings_to_string.ts @@ -13,17 +13,27 @@ import { AgentConfiguration } from '../../../../common/agent_configuration/confi export function convertConfigSettingsToString( hit: SearchHit ) { - const config = hit._source; + const { settings } = hit._source; - if (config.settings?.transaction_sample_rate) { - config.settings.transaction_sample_rate = - config.settings.transaction_sample_rate.toString(); - } + const convertedConfigSettings = { + ...settings, + ...(settings?.transaction_sample_rate + ? { + transaction_sample_rate: settings.transaction_sample_rate.toString(), + } + : {}), + ...(settings?.transaction_max_spans + ? { + transaction_max_spans: settings.transaction_max_spans.toString(), + } + : {}), + }; - if (config.settings?.transaction_max_spans) { - config.settings.transaction_max_spans = - config.settings.transaction_max_spans.toString(); - } - - return hit; + return { + ...hit, + _source: { + ...hit._source, + settings: convertedConfigSettings, + }, + }; } diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts index 18e2fe0f34a6d4..f32e53a1ad1dd0 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/find_exact_configuration.ts @@ -13,6 +13,7 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../lib/helpers/setup_request'; import { convertConfigSettingsToString } from './convert_settings_to_string'; +import { getConfigsAppliedToAgentsThroughFleet } from './get_config_applied_to_agent_through_fleet'; export async function findExactConfiguration({ service, @@ -40,16 +41,27 @@ export async function findExactConfiguration({ }, }; - const resp = await internalClient.search( - 'find_exact_agent_configuration', - params - ); + const [agentConfig, configsAppliedToAgentsThroughFleet] = await Promise.all([ + internalClient.search( + 'find_exact_agent_configuration', + params + ), + getConfigsAppliedToAgentsThroughFleet({ setup }), + ]); - const hit = resp.hits.hits[0] as SearchHit | undefined; + const hit = agentConfig.hits.hits[0] as + | SearchHit + | undefined; if (!hit) { return; } - return convertConfigSettingsToString(hit); + return { + id: hit._id, + ...convertConfigSettingsToString(hit)._source, + applied_by_agent: + hit._source.applied_by_agent || + configsAppliedToAgentsThroughFleet.hasOwnProperty(hit._source.etag), + }; } diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_config_applied_to_agent_through_fleet.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_config_applied_to_agent_through_fleet.ts new file mode 100644 index 00000000000000..351c21b43c1e9c --- /dev/null +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/get_config_applied_to_agent_through_fleet.ts @@ -0,0 +1,60 @@ +/* + * 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 { termQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import datemath from '@kbn/datemath'; +import { METRICSET_NAME } from '../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../lib/helpers/setup_request'; + +export async function getConfigsAppliedToAgentsThroughFleet({ + setup, +}: { + setup: Setup; +}) { + const { internalClient, indices } = setup; + + const params = { + index: indices.metric, + size: 0, + body: { + query: { + bool: { + filter: [ + ...termQuery(METRICSET_NAME, 'agent_config'), + ...rangeQuery( + datemath.parse('now-15m')!.valueOf(), + datemath.parse('now')!.valueOf() + ), + ], + }, + }, + aggs: { + config_by_etag: { + terms: { + field: 'labels.etag', + size: 200, + }, + }, + }, + }, + }; + + const response = await internalClient.search( + 'get_config_applied_to_agent_through_fleet', + params + ); + + return ( + response.aggregations?.config_by_etag.buckets.reduce( + (configsAppliedToAgentsThroughFleet, bucket) => { + configsAppliedToAgentsThroughFleet[bucket.key as string] = true; + return configsAppliedToAgentsThroughFleet; + }, + {} as Record + ) ?? {} + ); +} diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts index bc105106cb5e4f..416cb50c0a8014 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/list_configurations.ts @@ -8,6 +8,7 @@ import { Setup } from '../../../lib/helpers/setup_request'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { convertConfigSettingsToString } from './convert_settings_to_string'; +import { getConfigsAppliedToAgentsThroughFleet } from './get_config_applied_to_agent_through_fleet'; export async function listConfigurations({ setup }: { setup: Setup }) { const { internalClient, indices } = setup; @@ -17,12 +18,22 @@ export async function listConfigurations({ setup }: { setup: Setup }) { size: 200, }; - const resp = await internalClient.search( - 'list_agent_configuration', - params - ); + const [agentConfigs, configsAppliedToAgentsThroughFleet] = await Promise.all([ + internalClient.search( + 'list_agent_configuration', + params + ), + getConfigsAppliedToAgentsThroughFleet({ setup }), + ]); - return resp.hits.hits + return agentConfigs.hits.hits .map(convertConfigSettingsToString) - .map((hit) => hit._source); + .map((hit) => { + return { + ...hit._source, + applied_by_agent: + hit._source.applied_by_agent || + configsAppliedToAgentsThroughFleet.hasOwnProperty(hit._source.etag), + }; + }); } diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts index 72869ef165fa24..3d9abebeeef2b8 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration/route.ts @@ -38,7 +38,9 @@ const agentConfigurationRoute = createApmServerRoute({ >; }> => { const setup = await setupRequest(resources); + const configurations = await listConfigurations({ setup }); + return { configurations }; }, }); @@ -71,7 +73,7 @@ const getSingleAgentConfigurationRoute = createApmServerRoute({ throw Boom.notFound(); } - return config._source; + return config; }, }); @@ -102,11 +104,11 @@ const deleteAgentConfigurationRoute = createApmServerRoute({ } logger.info( - `Deleting config ${service.name}/${service.environment} (${config._id})` + `Deleting config ${service.name}/${service.environment} (${config.id})` ); const deleteConfigurationResult = await deleteConfiguration({ - configurationId: config._id, + configurationId: config.id, setup, }); @@ -162,7 +164,7 @@ const createOrUpdateAgentConfigurationRoute = createApmServerRoute({ ); await createOrUpdateConfiguration({ - configurationId: config?._id, + configurationId: config?.id, configurationIntake: body, setup, }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx index 540402b986e5b2..9fd7806d27665d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx @@ -129,7 +129,12 @@ const ComplianceTrendChart = ({ trend }: { trend: PostureTrend[] }) => { xAccessor={'timestamp'} yAccessors={['postureScore']} /> - + ({ index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, - size: 5, + size: 99, sort: '@timestamp:desc', + query: { + bool: { + must: { + range: { + '@timestamp': { + gte: 'now-1d', + lte: 'now', + }, + }, + }, + }, + }, }); export type Trends = Array<{ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 1ffb6c74d25fa3..9e39b86242a90a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -366,21 +366,90 @@ export const SOURCE_OBJ_TYPES = { }; export const SOURCE_CATEGORIES = { + ACCOUNT_MANAGEMENT: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.accountManagement', + { + defaultMessage: 'Account management', + } + ), + ATLASSIAN: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.atlassian', { + defaultMessage: 'Atlassian', + }), + BUG_TRACKING: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.bugTracking', + { + defaultMessage: 'Bug tracking', + } + ), + CHAT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.chat', { + defaultMessage: 'Chat', + }), CLOUD: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.cloud', { defaultMessage: 'Cloud', }), - COMMUNICATIONS: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.categories.communications', + CODE_REPOSITORY: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.codeRepository', + { + defaultMessage: 'Code repository', + } + ), + COLLABORATION: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.collaboration', + { + defaultMessage: 'Collaboration', + } + ), + COMMUNICATION: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.communication', + { + defaultMessage: 'Communication', + } + ), + CRM: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.crm', { + defaultMessage: 'CRM', + }), + CUSTOMER_RELATIONSHIP_MANAGEMENT: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.customerRelationshipManagement', + { + defaultMessage: 'Customer relationship management', + } + ), + CUSTOMER_SERVICE: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.customerService', { - defaultMessage: 'Communications', + defaultMessage: 'Customer service', } ), + EMAIL: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.email', { + defaultMessage: 'Email', + }), FILE_SHARING: i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.categories.fileSharing', { - defaultMessage: 'File Sharing', + defaultMessage: 'File sharing', + } + ), + GOOGLE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.google', { + defaultMessage: 'Google', + }), + GSUITE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.gsuite', { + defaultMessage: 'GSuite', + }), + HELP: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.help', { + defaultMessage: 'Help', + }), + HELPDESK: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.helpdesk', { + defaultMessage: 'Helpdesk', + }), + INSTANT_MESSAGING: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.instantMessaging', + { + defaultMessage: 'Instant messaging', } ), + INTRANET: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.intranet', { + defaultMessage: 'Intranet', + }), MICROSOFT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.microsoft', { defaultMessage: 'Microsoft', }), @@ -393,9 +462,33 @@ export const SOURCE_CATEGORIES = { defaultMessage: 'Productivity', } ), + PROJECT_MANAGEMENT: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.projectManagement', + { + defaultMessage: 'Project management', + } + ), + SOFTWARE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.software', { + defaultMessage: 'Software', + }), STORAGE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.storage', { defaultMessage: 'Storage', }), + TICKETING: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.ticketing', { + defaultMessage: 'Ticketing', + }), + VERSION_CONTROL: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.categories.versionControl', + { + defaultMessage: 'Version control', + } + ), + WIKI: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.wiki', { + defaultMessage: 'Wiki', + }), + WORKFLOW: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.workflow', { + defaultMessage: 'Workflow', + }), }; export const API_KEYS_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 181cd8b7c9a736..6188c37b20057a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -42,6 +42,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.BOX, serviceType: 'box', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -69,6 +74,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.CONFLUENCE, serviceType: 'confluence_cloud', + categories: [SOURCE_CATEGORIES.WIKI, SOURCE_CATEGORIES.ATLASSIAN, SOURCE_CATEGORIES.INTRANET], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -103,6 +109,7 @@ export const staticSourceData: SourceDataItem[] = [ name: SOURCE_NAMES.CONFLUENCE_CONNECTOR_PACKAGE, serviceType: 'external', baseServiceType: 'confluence_cloud', + categories: [SOURCE_CATEGORIES.WIKI, SOURCE_CATEGORIES.ATLASSIAN, SOURCE_CATEGORIES.INTRANET], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -136,6 +143,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.CONFLUENCE_SERVER, serviceType: 'confluence_server', + categories: [SOURCE_CATEGORIES.WIKI, SOURCE_CATEGORIES.ATLASSIAN, SOURCE_CATEGORIES.INTRANET], configuration: { isPublicKey: true, hasOauthRedirect: true, @@ -166,6 +174,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.DROPBOX, serviceType: 'dropbox', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -193,6 +206,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GITHUB, serviceType: 'github', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.VERSION_CONTROL, + SOURCE_CATEGORIES.CODE_REPOSITORY, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -227,6 +245,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GITHUB_ENTERPRISE, serviceType: 'github_enterprise_server', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.VERSION_CONTROL, + SOURCE_CATEGORIES.CODE_REPOSITORY, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -267,6 +290,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GMAIL, serviceType: 'gmail', + categories: [ + SOURCE_CATEGORIES.COMMUNICATION, + SOURCE_CATEGORIES.EMAIL, + SOURCE_CATEGORIES.GOOGLE, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -283,6 +311,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.GOOGLE_DRIVE, serviceType: 'google_drive', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.PRODUCTIVITY, + SOURCE_CATEGORIES.GSUITE, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -314,6 +349,12 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.JIRA, serviceType: 'jira_cloud', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.BUG_TRACKING, + SOURCE_CATEGORIES.ATLASSIAN, + SOURCE_CATEGORIES.PROJECT_MANAGEMENT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -348,6 +389,12 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.JIRA_SERVER, serviceType: 'jira_server', + categories: [ + SOURCE_CATEGORIES.SOFTWARE, + SOURCE_CATEGORIES.BUG_TRACKING, + SOURCE_CATEGORIES.ATLASSIAN, + SOURCE_CATEGORIES.PROJECT_MANAGEMENT, + ], configuration: { isPublicKey: true, hasOauthRedirect: true, @@ -396,6 +443,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.ONEDRIVE, serviceType: 'one_drive', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.MICROSOFT, + SOURCE_CATEGORIES.OFFICE_365, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -423,7 +477,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.OUTLOOK, categories: [ - SOURCE_CATEGORIES.COMMUNICATIONS, + SOURCE_CATEGORIES.COMMUNICATION, SOURCE_CATEGORIES.PRODUCTIVITY, SOURCE_CATEGORIES.MICROSOFT, ], @@ -442,6 +496,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SALESFORCE, serviceType: 'salesforce', + categories: [ + SOURCE_CATEGORIES.CRM, + SOURCE_CATEGORIES.CUSTOMER_RELATIONSHIP_MANAGEMENT, + SOURCE_CATEGORIES.ACCOUNT_MANAGEMENT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -476,6 +535,11 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SALESFORCE_SANDBOX, serviceType: 'salesforce_sandbox', + categories: [ + SOURCE_CATEGORIES.CRM, + SOURCE_CATEGORIES.CUSTOMER_RELATIONSHIP_MANAGEMENT, + SOURCE_CATEGORIES.ACCOUNT_MANAGEMENT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -510,6 +574,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SERVICENOW, serviceType: 'service_now', + categories: [SOURCE_CATEGORIES.WORKFLOW], configuration: { isPublicKey: false, hasOauthRedirect: false, @@ -542,6 +607,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SHAREPOINT, serviceType: 'share_point', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.MICROSOFT, + SOURCE_CATEGORIES.OFFICE_365, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -570,6 +642,13 @@ export const staticSourceData: SourceDataItem[] = [ name: SOURCE_NAMES.SHAREPOINT_CONNECTOR_PACKAGE, serviceType: 'external', baseServiceType: 'share_point', + categories: [ + SOURCE_CATEGORIES.FILE_SHARING, + SOURCE_CATEGORIES.STORAGE, + SOURCE_CATEGORIES.CLOUD, + SOURCE_CATEGORIES.MICROSOFT, + SOURCE_CATEGORIES.OFFICE_365, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -619,6 +698,12 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.SLACK, serviceType: 'slack', + categories: [ + SOURCE_CATEGORIES.COLLABORATION, + SOURCE_CATEGORIES.COMMUNICATION, + SOURCE_CATEGORIES.INSTANT_MESSAGING, + SOURCE_CATEGORIES.CHAT, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -639,7 +724,7 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.TEAMS, categories: [ - SOURCE_CATEGORIES.COMMUNICATIONS, + SOURCE_CATEGORIES.COMMUNICATION, SOURCE_CATEGORIES.PRODUCTIVITY, SOURCE_CATEGORIES.MICROSOFT, ], @@ -658,6 +743,13 @@ export const staticSourceData: SourceDataItem[] = [ { name: SOURCE_NAMES.ZENDESK, serviceType: 'zendesk', + categories: [ + SOURCE_CATEGORIES.HELP, + SOURCE_CATEGORIES.CUSTOMER_SERVICE, + SOURCE_CATEGORIES.CUSTOMER_RELATIONSHIP_MANAGEMENT, + SOURCE_CATEGORIES.TICKETING, + SOURCE_CATEGORIES.HELPDESK, + ], configuration: { isPublicKey: false, hasOauthRedirect: true, @@ -684,7 +776,7 @@ export const staticSourceData: SourceDataItem[] = [ }, { name: SOURCE_NAMES.ZOOM, - categories: [SOURCE_CATEGORIES.COMMUNICATIONS, SOURCE_CATEGORIES.PRODUCTIVITY], + categories: [SOURCE_CATEGORIES.COMMUNICATION, SOURCE_CATEGORIES.PRODUCTIVITY], serviceType: 'custom', baseServiceType: 'zoom', configuration: { diff --git a/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts index 21d3584b9fc468..4ef3e263df01c6 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts @@ -34,10 +34,9 @@ describe('Alert Event Details', () => { runKbnArchiverScript(ArchiverMethod.UNLOAD, 'rule'); }); - it('should be able to run live query', () => { + it('should prepare packs and alert rules', () => { const PACK_NAME = 'testpack'; const RULE_NAME = 'Test-rule'; - const TIMELINE_NAME = 'Untitled timeline'; navigateTo('/app/osquery/packs'); preparePack(PACK_NAME); findAndClickButton('Edit'); @@ -57,8 +56,14 @@ describe('Alert Event Details', () => { cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false'); cy.getBySel('ruleSwitch').click(); cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + }); + + it('should be able to run live query and add to timeline (-depending on the previous test)', () => { + const TIMELINE_NAME = 'Untitled timeline'; cy.visit('/app/security/alerts'); - cy.wait(500); + cy.getBySel('header-page-title').contains('Alerts').should('exist'); + cy.getBySel('timeline-context-menu-button').first().click({ force: true }); + cy.getBySel('osquery-action-item').should('exist').contains('Run Osquery'); cy.getBySel('expand-event').first().click(); cy.getBySel('take-action-dropdown-btn').click(); cy.getBySel('osquery-action-item').click(); diff --git a/x-pack/plugins/security_solution/common/ecs/agent/index.ts b/x-pack/plugins/security_solution/common/ecs/agent/index.ts index 2332b60f1a3cad..7084214a9b8763 100644 --- a/x-pack/plugins/security_solution/common/ecs/agent/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/agent/index.ts @@ -7,4 +7,5 @@ export interface AgentEcs { type?: string[]; + id?: string[]; } diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts index 0db0699628cc08..ba86842106e239 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts @@ -48,7 +48,7 @@ export const useFormatUrl = (page: SecurityPageName) => { return { formatUrl, search }; }; -type GetSecuritySolutionUrl = (param: { +export type GetSecuritySolutionUrl = (param: { deepLinkId: SecurityPageName; path?: string; absolute?: boolean; @@ -63,6 +63,7 @@ export const useGetSecuritySolutionUrl = () => { ({ deepLinkId, path = '', absolute = false, skipSearch = false }) => { const search = needsUrlState(deepLinkId) ? getUrlStateQueryString() : ''; const formattedPath = formatPath(path, search, skipSearch); + return getAppUrl({ deepLinkId, path: formattedPath, absolute }); }, [getAppUrl, getUrlStateQueryString] diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts new file mode 100644 index 00000000000000..c70d7d24fcb949 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChromeBreadcrumb } from '@kbn/core/public'; +import { SecurityPageName } from '../../../../app/types'; +import { APP_NAME } from '../../../../../common/constants'; +import { getAppLandingUrl } from '../../link_to/redirect_to_landing'; + +import { GetSecuritySolutionUrl } from '../../link_to'; +import { getAncestorLinksInfo } from '../../../links'; +import { GenericNavRecord } from '../types'; + +export const getLeadingBreadcrumbsForSecurityPage = ( + pageName: SecurityPageName, + getSecuritySolutionUrl: GetSecuritySolutionUrl, + navTabs: GenericNavRecord, + isGroupedNavigationEnabled: boolean +): [ChromeBreadcrumb, ...ChromeBreadcrumb[]] => { + const landingPath = getSecuritySolutionUrl({ deepLinkId: SecurityPageName.landing }); + + const siemRootBreadcrumb: ChromeBreadcrumb = { + text: APP_NAME, + href: getAppLandingUrl(landingPath), + }; + + const breadcrumbs: ChromeBreadcrumb[] = getAncestorLinksInfo(pageName).map(({ title, id }) => { + const newTitle = title; + // Get title from navTabs because pages title on the new structure might be different. + const oldTitle = navTabs[id] ? navTabs[id].name : title; + + return { + text: isGroupedNavigationEnabled ? newTitle : oldTitle, + href: getSecuritySolutionUrl({ deepLinkId: id }), + }; + }); + + return [siemRootBreadcrumb, ...breadcrumbs]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index 7d2bfaa405cb2e..05dd7145ba7850 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -7,15 +7,35 @@ import '../../../mock/match_media'; import { encodeIpv6 } from '../../../lib/helpers'; -import { getBreadcrumbsForRoute, useSetBreadcrumbs } from '.'; +import { getBreadcrumbsForRoute, ObjectWithNavTabs, useSetBreadcrumbs } from '.'; import { HostsTableType } from '../../../../hosts/store/model'; import { RouteSpyState, SiemRouteType } from '../../../utils/route/types'; -import { TabNavigationProps } from '../tab_navigation/types'; import { NetworkRouteType } from '../../../../network/pages/navigation/types'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { AdministrationSubTab } from '../../../../management/types'; import { renderHook } from '@testing-library/react-hooks'; import { TestProviders } from '../../../mock'; +import { GetSecuritySolutionUrl } from '../../link_to'; +import { APP_UI_ID } from '../../../../../common/constants'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { useIsGroupedNavigationEnabled } from '../helpers'; +import { navTabs } from '../../../../app/home/home_navigations'; +import { getAppLinks } from '../../../links/app_links'; +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { StartPlugins } from '../../../../types'; +import { coreMock } from '@kbn/core/public/mocks'; +import { updateAppLinks } from '../../../links'; + +jest.mock('../../../hooks/use_selector'); + +const mockUseIsGroupedNavigationEnabled = useIsGroupedNavigationEnabled as jest.Mock; +jest.mock('../helpers', () => { + const original = jest.requireActual('../helpers'); + return { + ...original, + useIsGroupedNavigationEnabled: jest.fn(), + }; +}); const setBreadcrumbsMock = jest.fn(); const chromeMock = { @@ -40,412 +60,824 @@ const getMockObject = ( pageName: string, pathName: string, detailName: string | undefined -): RouteSpyState & TabNavigationProps => ({ +): RouteSpyState & ObjectWithNavTabs => ({ detailName, - navTabs: { - cases: { - disabled: false, - href: '/app/security/cases', - id: 'cases', - name: 'Cases', - urlKey: 'cases', - }, - hosts: { - disabled: false, - href: '/app/security/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '/app/security/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '/app/security/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '/app/security/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - alerts: { - disabled: false, - href: '/app/security/alerts', - id: 'alerts', - name: 'Alerts', - urlKey: 'alerts', - }, - exceptions: { - disabled: false, - href: '/app/security/exceptions', - id: 'exceptions', - name: 'Exceptions', - urlKey: 'exceptions', - }, - rules: { - disabled: false, - href: '/app/security/rules', - id: 'rules', - name: 'Rules', - urlKey: 'rules', - }, - }, + navTabs, pageName, pathName, search: '', tabName: mockDefaultTab(pageName) as HostsTableType, - query: { query: '', language: 'kuery' }, - filters: [], - timeline: { - activeTab: TimelineTabs.query, - id: '', - isOpen: false, - graphEventId: '', - }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', +}); + +(useDeepEqualSelector as jest.Mock).mockImplementation(() => { + return { + urlState: { + query: { query: '', language: 'kuery' }, + filters: [], + timeline: { + activeTab: TimelineTabs.query, + id: '', + isOpen: false, + graphEventId: '', }, - }, - timeline: { - linkTo: ['global'], timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', + global: { + linkTo: ['timeline'], + timerange: { + from: '2019-05-16T23:10:43.696Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2019-05-17T23:10:43.697Z', + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: '2019-05-16T23:10:43.696Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2019-05-17T23:10:43.697Z', + toStr: 'now', + }, + }, }, + sourcerer: {}, }, - }, - sourcerer: {}, + }; }); -// The string returned is different from what getUrlForApp returns, but does not matter for the purposes of this test. -const getUrlForAppMock = ( - appId: string, - options?: { deepLinkId?: string; path?: string; absolute?: boolean } -) => `${appId}${options?.deepLinkId ? `/${options.deepLinkId}` : ''}${options?.path ?? ''}`; +// The string returned is different from what getSecuritySolutionUrl returns, but does not matter for the purposes of this test. +const getSecuritySolutionUrl: GetSecuritySolutionUrl = ({ + deepLinkId, + path, +}: { + deepLinkId?: string; + path?: string; + absolute?: boolean; +}) => `${APP_UI_ID}${deepLinkId ? `/${deepLinkId}` : ''}${path ?? ''}`; + +jest.mock('../../../lib/kibana/kibana_react', () => { + return { + useKibana: () => ({ + services: { + chrome: undefined, + application: { + navigateToApp: jest.fn(), + getUrlForApp: (appId: string, options?: { path?: string; deepLinkId?: boolean }) => + `${appId}/${options?.deepLinkId ?? ''}${options?.path ?? ''}`, + }, + }, + }), + }; +}); describe('Navigation Breadcrumbs', () => { + beforeAll(async () => { + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); + const hostName = 'siem-kibana'; const ipv4 = '192.0.2.255'; const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; const ipv6Encoded = encodeIpv6(ipv6); - describe('getBreadcrumbsForRoute', () => { - test('should return Host breadcrumbs when supplied host pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { - href: 'securitySolutionUI/get_started', - text: 'Security', - }, - { - href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - text: 'Hosts', - }, - { - href: '', - text: 'Authentications', - }, - ]); + describe('Old Architecture', () => { + beforeAll(() => { + mockUseIsGroupedNavigationEnabled.mockReturnValue(false); }); - test('should return Network breadcrumbs when supplied network pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: "securitySolutionUI/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'Flows', - href: '', - }, - ]); - }); + describe('getBreadcrumbsForRoute', () => { + test('should return Overview breadcrumbs when supplied overview pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('overview', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: '', + text: 'Overview', + }, + ]); + }); - test('should return Timelines breadcrumbs when supplied timelines pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('timelines', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Timelines', - href: "securitySolutionUI/timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - ]); - }); + test('should return Host breadcrumbs when supplied hosts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: 'securitySolutionUI/hosts', + text: 'Hosts', + }, + { + href: '', + text: 'Authentications', + }, + ]); + }); - test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', hostName), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Hosts', - href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'siem-kibana', - href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { text: 'Authentications', href: '' }, - ]); - }); + test('should return Network breadcrumbs when supplied network pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: 'Flows', + href: '', + }, + ]); + }); - test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv4), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: "securitySolutionUI/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: ipv4, - href: `securitySolutionUI/network/ip/${ipv4}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - { text: 'Flows', href: '' }, - ]); - }); + test('should return Timelines breadcrumbs when supplied timelines pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('timelines', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Timelines', + href: '', + }, + ]); + }); - test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv6Encoded), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Network', - href: "securitySolutionUI/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: ipv6, - href: `securitySolutionUI/network/ip/${ipv6Encoded}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - { text: 'Flows', href: '' }, - ]); - }); + test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', hostName), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Hosts', + href: 'securitySolutionUI/hosts', + }, + { + text: 'siem-kibana', + href: 'securitySolutionUI/hosts/siem-kibana', + }, + { text: 'Authentications', href: '' }, + ]); + }); - test('should return Alerts breadcrumbs when supplied alerts pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('alerts', '/alerts', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Alerts', - href: '', - }, - ]); - }); + test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv4), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv4, + href: `securitySolutionUI/network/ip/${ipv4}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); - test('should return Exceptions breadcrumbs when supplied exceptions pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('exceptions', '/exceptions', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Exceptions', - href: '', - }, - ]); - }); + test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv6Encoded), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv6, + href: `securitySolutionUI/network/ip/${ipv6Encoded}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); - test('should return Rules breadcrumbs when supplied rules pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - ]); - }); + test('should return Alerts breadcrumbs when supplied alerts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('alerts', '/alerts', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Alerts', + href: '', + }, + ]); + }); - test('should return Rules breadcrumbs when supplied rules Creation pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules/create', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'Create', - href: '', - }, - ]); - }); + test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('exceptions', '/exceptions', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Exception lists', + href: '', + }, + ]); + }); - test('should return Rules breadcrumbs when supplied rules Details pathname', () => { - const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'ALERT_RULE_NAME'; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), - detailName: mockDetailName, - state: { - ruleName: mockRuleName, + test('should return Rules breadcrumbs when supplied rules pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: '', }, - }, - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: mockRuleName, - href: `securitySolutionUI/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - ]); - }); + ]); + }); - test('should return Rules breadcrumbs when supplied rules Edit pathname', () => { - const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'ALERT_RULE_NAME'; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), - detailName: mockDetailName, - state: { - ruleName: mockRuleName, + test('should return Rules breadcrumbs when supplied rules Creation pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules/create', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', }, - }, - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Rules', - href: "securitySolutionUI/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - }, - { - text: 'ALERT_RULE_NAME', - href: `securitySolutionUI/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, - }, - { - text: 'Edit', - href: '', - }, - ]); + { + text: 'Create', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Details pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: mockRuleName, + href: ``, + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Edit pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: 'ALERT_RULE_NAME', + href: `securitySolutionUI/rules/id/${mockDetailName}`, + }, + { + text: 'Edit', + href: '', + }, + ]); + }); + + test('should return null breadcrumbs when supplied Cases pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('cases', '/', undefined), + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return null breadcrumbs when supplied Cases details pageName', () => { + const sampleCase = { + id: 'my-case-id', + name: 'Case name', + }; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), + state: { caseTitle: sampleCase.name }, + }, + getSecuritySolutionUrl, + false + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return Admin breadcrumbs when supplied endpoints pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('administration', '/endpoints', undefined), + getSecuritySolutionUrl, + false + ); + + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Endpoints', + href: '', + }, + ]); + }); }); - test('should return null breadcrumbs when supplied Cases pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('cases', '/', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual(null); + describe('setBreadcrumbs()', () => { + test('should call chrome breadcrumb service with correct breadcrumbs', () => { + const navigateToUrlMock = jest.fn(); + const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); + result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock); + + expect(setBreadcrumbsMock).toBeCalledWith([ + expect.objectContaining({ + text: 'Security', + href: "securitySolutionUI/get_started?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'Hosts', + href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'siem-kibana', + href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + onClick: expect.any(Function), + }), + { + text: 'Authentications', + href: '', + }, + ]); + }); }); + }); - test('should return null breadcrumbs when supplied Cases details pathname', () => { - const sampleCase = { - id: 'my-case-id', - name: 'Case name', - }; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), - state: { caseTitle: sampleCase.name }, - }, - getUrlForAppMock - ); - expect(breadcrumbs).toEqual(null); + describe('New Architecture', () => { + beforeAll(() => { + mockUseIsGroupedNavigationEnabled.mockReturnValue(true); }); - test('should return Admin breadcrumbs when supplied endpoints pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('administration', '/endpoints', undefined), - getUrlForAppMock - ); - expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionUI/get_started' }, - { - text: 'Endpoints', - href: '', - }, - ]); + describe('getBreadcrumbsForRoute', () => { + test('should return Overview breadcrumbs when supplied overview pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('overview', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: 'securitySolutionUI/dashboards', + text: 'Dashboards', + }, + { + href: '', + text: 'Overview', + }, + ]); + }); + + test('should return Host breadcrumbs when supplied hosts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { + href: 'securitySolutionUI/get_started', + text: 'Security', + }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + href: 'securitySolutionUI/hosts', + text: 'Hosts', + }, + { + href: '', + text: 'Authentications', + }, + ]); + }); + + test('should return Network breadcrumbs when supplied network pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: 'Flows', + href: '', + }, + ]); + }); + + test('should return Timelines breadcrumbs when supplied timelines pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('timelines', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Timelines', + href: '', + }, + ]); + }); + + test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', hostName), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Hosts', + href: 'securitySolutionUI/hosts', + }, + { + text: 'siem-kibana', + href: 'securitySolutionUI/hosts/siem-kibana', + }, + { text: 'Authentications', href: '' }, + ]); + }); + + test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv4), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv4, + href: `securitySolutionUI/network/ip/${ipv4}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); + + test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv6Encoded), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + href: 'securitySolutionUI/threat_hunting', + text: 'Threat Hunting', + }, + { + text: 'Network', + href: 'securitySolutionUI/network', + }, + { + text: ipv6, + href: `securitySolutionUI/network/ip/${ipv6Encoded}/source`, + }, + { text: 'Flows', href: '' }, + ]); + }); + + test('should return Alerts breadcrumbs when supplied alerts pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('alerts', '/alerts', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Alerts', + href: '', + }, + ]); + }); + + test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('exceptions', '/exceptions', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Exception lists', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Creation pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('rules', '/rules/create', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: 'Create', + href: '', + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Details pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: mockRuleName, + href: ``, + }, + ]); + }); + + test('should return Rules breadcrumbs when supplied rules Edit pageName', () => { + const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; + const mockRuleName = 'ALERT_RULE_NAME'; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Rules', + href: 'securitySolutionUI/rules', + }, + { + text: 'ALERT_RULE_NAME', + href: `securitySolutionUI/rules/id/${mockDetailName}`, + }, + { + text: 'Edit', + href: '', + }, + ]); + }); + + test('should return null breadcrumbs when supplied Cases pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('cases', '/', undefined), + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return null breadcrumbs when supplied Cases details pageName', () => { + const sampleCase = { + id: 'my-case-id', + name: 'Case name', + }; + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), + state: { caseTitle: sampleCase.name }, + }, + getSecuritySolutionUrl, + true + ); + expect(breadcrumbs).toEqual(null); + }); + + test('should return Admin breadcrumbs when supplied endpoints pageName', () => { + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('administration', '/endpoints', undefined), + getSecuritySolutionUrl, + true + ); + + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionUI/get_started' }, + { + text: 'Manage', + href: 'securitySolutionUI/administration', + }, + { + text: 'Endpoints', + href: '', + }, + ]); + }); }); - }); - describe('setBreadcrumbs()', () => { - test('should call chrome breadcrumb service with correct breadcrumbs', () => { - const navigateToUrlMock = jest.fn(); - const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); - result.current( - getMockObject('hosts', '/', hostName), - chromeMock, - getUrlForAppMock, - navigateToUrlMock - ); - expect(setBreadcrumbsMock).toBeCalledWith([ - expect.objectContaining({ - text: 'Security', - href: 'securitySolutionUI/get_started', - onClick: expect.any(Function), - }), - expect.objectContaining({ - text: 'Hosts', - href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - onClick: expect.any(Function), - }), - expect.objectContaining({ - text: 'siem-kibana', - href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", - onClick: expect.any(Function), - }), - { - text: 'Authentications', - href: '', - }, - ]); + describe('setBreadcrumbs()', () => { + test('should call chrome breadcrumb service with correct breadcrumbs', () => { + const navigateToUrlMock = jest.fn(); + const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); + result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock); + const searchString = + "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"; + + expect(setBreadcrumbsMock).toBeCalledWith([ + expect.objectContaining({ + text: 'Security', + href: `securitySolutionUI/get_started${searchString}`, + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'Threat Hunting', + href: `securitySolutionUI/threat_hunting`, + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'Hosts', + href: `securitySolutionUI/hosts${searchString}`, + onClick: expect.any(Function), + }), + expect.objectContaining({ + text: 'siem-kibana', + href: `securitySolutionUI/hosts/siem-kibana${searchString}`, + onClick: expect.any(Function), + }), + { + text: 'Authentications', + href: '', + }, + ]); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 3c2e103c0dfd3e..ba4835bf776c90 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -5,43 +5,50 @@ * 2.0. */ -import { getOr, omit } from 'lodash/fp'; +import { last, omit } from 'lodash/fp'; import { useDispatch } from 'react-redux'; import { ChromeBreadcrumb } from '@kbn/core/public'; -import { APP_NAME, APP_UI_ID } from '../../../../../common/constants'; import { StartServices } from '../../../../types'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/details'; -import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; -import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; -import { getBreadcrumbs as getUsersBreadcrumbs } from '../../../../users/pages/details/utils'; -import { getBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/common/breadcrumbs'; +import { getTrailingBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; +import { getTrailingBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/details'; +import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; +import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../users/pages/details/utils'; +import { getTrailingBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/common/breadcrumbs'; import { SecurityPageName } from '../../../../app/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState, - TimelineRouteSpyState, AdministrationRouteSpyState, UsersRouteSpyState, } from '../../../utils/route/types'; -import { getAppLandingUrl } from '../../link_to/redirect_to_landing'; import { timelineActions } from '../../../../timelines/store/timeline'; import { TimelineId } from '../../../../../common/types/timeline'; -import { TabNavigationProps } from '../tab_navigation/types'; -import { getSearch } from '../helpers'; -import { GetUrlForApp, NavigateToUrl, SearchNavTab } from '../types'; +import { GenericNavRecord, NavigateToUrl } from '../types'; +import { getLeadingBreadcrumbsForSecurityPage } from './get_breadcrumbs_for_page'; +import { GetSecuritySolutionUrl, useGetSecuritySolutionUrl } from '../../link_to'; +import { useIsGroupedNavigationEnabled } from '../helpers'; + +export interface ObjectWithNavTabs { + navTabs: GenericNavRecord; +} export const useSetBreadcrumbs = () => { const dispatch = useDispatch(); + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); + const isGroupedNavigationEnabled = useIsGroupedNavigationEnabled(); + return ( - spyState: RouteSpyState & TabNavigationProps, + spyState: RouteSpyState & ObjectWithNavTabs, chrome: StartServices['chrome'], - getUrlForApp: GetUrlForApp, navigateToUrl: NavigateToUrl ) => { - const breadcrumbs = getBreadcrumbsForRoute(spyState, getUrlForApp); + const breadcrumbs = getBreadcrumbsForRoute( + spyState, + getSecuritySolutionUrl, + isGroupedNavigationEnabled + ); if (breadcrumbs) { chrome.setBreadcrumbs( breadcrumbs.map((breadcrumb) => ({ @@ -64,158 +71,103 @@ export const useSetBreadcrumbs = () => { }; }; -const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.network; - -const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.hosts; - -const isUsersRoutes = (spyState: RouteSpyState): spyState is UsersRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.users; - -const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.timelines; - -const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.case; - -const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.administration; - -const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => - spyState != null && - (spyState.pageName === SecurityPageName.rules || - spyState.pageName === SecurityPageName.rulesCreate); - -// eslint-disable-next-line complexity export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps, - getUrlForApp: GetUrlForApp + object: RouteSpyState & ObjectWithNavTabs, + getSecuritySolutionUrl: GetSecuritySolutionUrl, + isGroupedNavigationEnabled: boolean ): ChromeBreadcrumb[] | null => { const spyState: RouteSpyState = omit('navTabs', object); - const landingPath = getUrlForApp(APP_UI_ID, { deepLinkId: SecurityPageName.landing }); - - const siemRootBreadcrumb: ChromeBreadcrumb = { - text: APP_NAME, - href: getAppLandingUrl(landingPath), - }; - if (isHostsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - siemRootBreadcrumb, - ...getHostDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; + if (!spyState || !object.navTabs || !spyState.pageName || isCaseRoutes(spyState)) { + return null; } - if (isNetworkRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + + const newMenuLeadingBreadcrumbs = getLeadingBreadcrumbsForSecurityPage( + spyState.pageName as SecurityPageName, + getSecuritySolutionUrl, + object.navTabs, + isGroupedNavigationEnabled + ); + + // last newMenuLeadingBreadcrumbs is the current page + const pageBreadcrumb = newMenuLeadingBreadcrumbs[newMenuLeadingBreadcrumbs.length - 1]; + const siemRootBreadcrumb = newMenuLeadingBreadcrumbs[0]; + + const leadingBreadcrumbs = isGroupedNavigationEnabled + ? newMenuLeadingBreadcrumbs + : [siemRootBreadcrumb, pageBreadcrumb]; + + // Admin URL works differently. All admin pages are under '/administration' + if (isAdminRoutes(spyState)) { + if (isGroupedNavigationEnabled) { + return emptyLastBreadcrumbUrl([...leadingBreadcrumbs, ...getAdminBreadcrumbs(spyState)]); + } else { + return [ + ...(siemRootBreadcrumb ? [siemRootBreadcrumb] : []), + ...getAdminBreadcrumbs(spyState), + ]; } - return [ - siemRootBreadcrumb, - ...getIPDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; } - if (isUsersRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'users', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } + return emptyLastBreadcrumbUrl([ + ...leadingBreadcrumbs, + ...getTrailingBreadcrumbsForRoutes(spyState, getSecuritySolutionUrl), + ]); +}; - return [ - siemRootBreadcrumb, - ...getUsersBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; +const getTrailingBreadcrumbsForRoutes = ( + spyState: RouteSpyState, + getSecuritySolutionUrl: GetSecuritySolutionUrl +): ChromeBreadcrumb[] => { + if (isHostsRoutes(spyState)) { + return getHostDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); + } + if (isNetworkRoutes(spyState)) { + return getIPDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); } - if (isRulesRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: SecurityPageName.rules, isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - siemRootBreadcrumb, - ...getDetectionRulesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; + if (isUsersRoutes(spyState)) { + return getUsersBreadcrumbs(spyState, getSecuritySolutionUrl); } - if (isCaseRoutes(spyState) && object.navTabs) { - return null; // controlled by Cases routes + if (isRulesRoutes(spyState)) { + return getDetectionRulesBreadcrumbs(spyState, getSecuritySolutionUrl); } - if (isTimelinesRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'timeline', isDetailPage: false }; - const urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + return []; +}; - return [ - siemRootBreadcrumb, - ...getTimelinesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ), - getUrlForApp - ), - ]; - } +const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => + spyState.pageName === SecurityPageName.network; - if (isAdminRoutes(spyState) && object.navTabs) { - return [siemRootBreadcrumb, ...getAdminBreadcrumbs(spyState)]; - } +const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => + spyState.pageName === SecurityPageName.hosts; + +const isUsersRoutes = (spyState: RouteSpyState): spyState is UsersRouteSpyState => + spyState.pageName === SecurityPageName.users; + +const isCaseRoutes = (spyState: RouteSpyState) => spyState.pageName === SecurityPageName.case; + +const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => + spyState.pageName === SecurityPageName.administration; + +const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => + spyState.pageName === SecurityPageName.rules || + spyState.pageName === SecurityPageName.rulesCreate; + +const emptyLastBreadcrumbUrl = (breadcrumbs: ChromeBreadcrumb[]) => { + const leadingBreadCrumbs = breadcrumbs.slice(0, -1); + const lastBreadcrumb = last(breadcrumbs); - if ( - spyState != null && - object.navTabs && - spyState.pageName && - object.navTabs[spyState.pageName] - ) { + if (lastBreadcrumb) { return [ - siemRootBreadcrumb, + ...leadingBreadCrumbs, { - text: object.navTabs[spyState.pageName].name, + ...lastBreadcrumb, href: '', }, ]; } - return null; + return breadcrumbs; }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts index 5569d8c85afa8e..b2d91492b3ae1e 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts @@ -9,8 +9,6 @@ import { isEmpty } from 'lodash/fp'; import { Location } from 'history'; import type { Filter, Query } from '@kbn/es-query'; -import { useUiSetting$ } from '../../lib/kibana'; -import { ENABLE_GROUPED_NAVIGATION } from '../../../../common/constants'; import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { CONSTANTS } from '../url_state/constants'; @@ -24,6 +22,8 @@ import { import { SearchNavTab } from './types'; import { SourcererUrlState } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { useUiSetting$ } from '../../lib/kibana'; +import { ENABLE_GROUPED_NAVIGATION } from '../../../../common/constants'; export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { if (tab && tab.urlKey != null && !isAdministration(tab.urlKey)) { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index d14c8a51a66ee3..f70b77b15dc8c7 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -111,44 +111,12 @@ describe('SIEM Navigation', () => { pageName: 'hosts', pathName: '/', search: '', - sourcerer: {}, state: undefined, tabName: 'authentications', - query: { query: '', language: 'kuery' }, - filters: [], flowTarget: undefined, savedQuery: undefined, - timeline: { - activeTab: TimelineTabs.query, - id: '', - isOpen: false, - graphEventId: '', - }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - }, }, undefined, - mockGetUrlForApp, mockNavigateToUrl ); }); @@ -163,43 +131,15 @@ describe('SIEM Navigation', () => { 2, { detailName: undefined, - filters: [], flowTarget: undefined, navTabs, + search: '', pageName: 'network', pathName: '/', - query: { language: 'kuery', query: '' }, - savedQuery: undefined, - search: '', - sourcerer: {}, state: undefined, tabName: 'authentications', - timeline: { id: '', isOpen: false, activeTab: TimelineTabs.query, graphEventId: '' }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: '2019-05-16T23:10:43.696Z', - fromStr: 'now-24h', - kind: 'relative', - to: '2019-05-17T23:10:43.697Z', - toStr: 'now', - }, - }, - }, }, undefined, - mockGetUrlForApp, mockNavigateToUrl ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index f8b9251f4ff916..8491171e65bca2 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -49,22 +49,15 @@ export const TabNavigationComponent: React.FC< setBreadcrumbs( { detailName, - filters: urlState.filters, flowTarget, navTabs, pageName, pathName, - query: urlState.query, - savedQuery: urlState.savedQuery, search, - sourcerer: urlState.sourcerer, state, tabName, - timeline: urlState.timeline, - timerange: urlState.timerange, }, chrome, - getUrlForApp, navigateToUrl ); } @@ -74,7 +67,6 @@ export const TabNavigationComponent: React.FC< pathName, search, navTabs, - urlState, state, detailName, flowTarget, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx index 870ab15906f711..c20cf6414ae5d2 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -45,22 +45,15 @@ export const useSecuritySolutionNavigation = () => { setBreadcrumbs( { detailName, - filters: urlState.filters, flowTarget, navTabs: enabledNavTabs, pageName, pathName, - query: urlState.query, - savedQuery: urlState.savedQuery, search, - sourcerer: urlState.sourcerer, state, tabName, - timeline: urlState.timeline, - timerange: urlState.timerange, }, chrome, - getUrlForApp, navigateToUrl ); } @@ -69,7 +62,6 @@ export const useSecuritySolutionNavigation = () => { pageName, pathName, search, - urlState, state, detailName, flowTarget, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index faf3fe10da0795..cb49215ee8c9c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -25,6 +25,11 @@ import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; import { waitFor } from '@testing-library/react'; import { useLocation } from 'react-router-dom'; +import { updateAppLinks } from '../../links'; +import { getAppLinks } from '../../links/app_links'; +import { StartPlugins } from '../../../types'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; +import { coreMock } from '@kbn/core/public/mocks'; let mockProps: UrlStateContainerPropTypes; @@ -78,10 +83,36 @@ jest.mock('react-router-dom', () => { }; }); +const mockedUseIsGroupedNavigationEnabled = jest.fn(); +jest.mock('../navigation/helpers', () => ({ + useIsGroupedNavigationEnabled: () => mockedUseIsGroupedNavigationEnabled(), +})); + describe('UrlStateContainer', () => { + beforeAll(async () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); + + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); + afterEach(() => { jest.clearAllMocks(); + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); }); + describe('handleInitialize', () => { describe('URL state updates redux', () => { describe('relative timerange actions are called with correct data on component mount', () => { @@ -226,6 +257,44 @@ describe('UrlStateContainer', () => { expect(mockHistory.replace).not.toHaveBeenCalled(); }); + it("it doesn't update URL state when on admin page and grouped nav disabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); + mockProps = getMockPropsObj({ + page: CONSTANTS.unknown, + examplePath: '/administration', + namespaceLower: 'administration', + pageName: SecurityPageName.administration, + detailName: undefined, + }).noSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + + mount( useUrlStateHooks(args)} />); + + expect(mockHistory.replace.mock.calls[0][0].search).toBe('?'); + }); + + it("it doesn't update URL state when on admin page and grouped nav enabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(true); + mockProps = getMockPropsObj({ + page: CONSTANTS.unknown, + examplePath: '/dashboards', + namespaceLower: 'dashboards', + pageName: SecurityPageName.dashboardsLanding, + detailName: undefined, + }).noSearch.undefinedQuery; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + + mount( useUrlStateHooks(args)} />); + + expect(mockHistory.replace.mock.calls[0][0].search).toBe('?'); + }); + it('it removes empty AppQuery state from URL', () => { mockProps = { ...getMockProps( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index 4063ecdb739352..011621b95a0c4d 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -16,7 +16,12 @@ import { getFilterQuery, getMockPropsObj, mockHistory, testCases } from './test_ import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; import { useLocation } from 'react-router-dom'; -import { MANAGEMENT_PATH } from '../../../../common/constants'; +import { DASHBOARDS_PATH, MANAGEMENT_PATH } from '../../../../common/constants'; +import { getAppLinks } from '../../links/app_links'; +import { StartPlugins } from '../../../types'; +import { updateAppLinks } from '../../links'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; +import { coreMock } from '@kbn/core/public/mocks'; let mockProps: UrlStateContainerPropTypes; @@ -45,7 +50,31 @@ jest.mock('react-redux', () => { }; }); +const mockedUseIsGroupedNavigationEnabled = jest.fn(); +jest.mock('../navigation/helpers', () => ({ + useIsGroupedNavigationEnabled: () => mockedUseIsGroupedNavigationEnabled(), +})); + describe('UrlStateContainer - lodash.throttle mocked to test update url', () => { + beforeAll(async () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); + + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); + afterEach(() => { jest.clearAllMocks(); jest.resetAllMocks(); @@ -210,7 +239,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => }); }); - test("administration page doesn't has query string", () => { + test("administration page doesn't has query string when grouped nav disabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); mockProps = getMockPropsObj({ page: CONSTANTS.networkPage, examplePath: '/network', @@ -285,6 +315,83 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => state: '', }); }); + + test("dashboards page doesn't has query string when grouped nav enabled", () => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(true); + mockProps = getMockPropsObj({ + page: CONSTANTS.networkPage, + examplePath: '/network', + namespaceLower: 'network', + pageName: SecurityPageName.network, + detailName: undefined, + }).noSearch.definedQuery; + + const urlState = { + ...mockProps.urlState, + [CONSTANTS.appQuery]: getFilterQuery(), + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: '2020-07-07T08:20:18.966Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2020-07-08T08:20:18.966Z', + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: '2020-07-07T08:20:18.966Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2020-07-08T08:20:18.966Z', + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + }; + + const updatedMockProps = { + ...getMockPropsObj({ + ...mockProps, + page: CONSTANTS.unknown, + examplePath: DASHBOARDS_PATH, + namespaceLower: 'dashboards', + pageName: SecurityPageName.dashboardsLanding, + detailName: undefined, + }).noSearch.definedQuery, + urlState, + }; + + (useLocation as jest.Mock).mockReturnValue({ + pathname: mockProps.pathName, + }); + + const wrapper = mount( + useUrlStateHooks(args)} + /> + ); + + (useLocation as jest.Mock).mockReturnValue({ + pathname: updatedMockProps.pathName, + }); + + wrapper.setProps({ + hookProps: updatedMockProps, + }); + + wrapper.update(); + expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({ + hash: '', + pathname: DASHBOARDS_PATH, + search: '?', + state: '', + }); + }); }); describe('handleInitialize', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index 3245d647227ad9..e787b3a750e91c 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -40,6 +40,9 @@ import { import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { UrlInputsModel } from '../../store/inputs/model'; import { queryTimelineByIdOnUrlChange } from './query_timeline_by_id_on_url_change'; +import { getLinkInfo } from '../../links'; +import { SecurityPageName } from '../../../app/types'; +import { useIsGroupedNavigationEnabled } from '../navigation/helpers'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -62,7 +65,9 @@ export const useUrlStateHooks = ({ const { filterManager, savedQueries } = useKibana().services.data.query; const { pathname: browserPathName } = useLocation(); const prevProps = usePrevious({ pathName, pageName, urlState, search }); + const isGroupedNavEnabled = useIsGroupedNavigationEnabled(); + const linkInfo = pageName ? getLinkInfo(pageName as SecurityPageName) : undefined; const { setInitialStateFromUrl, updateTimeline, updateTimelineIsLoading } = useSetInitialStateFromUrl(); @@ -70,9 +75,10 @@ export const useUrlStateHooks = ({ (type: UrlStateType) => { const urlStateUpdatesToStore: UrlStateToRedux[] = []; const urlStateUpdatesToLocation: ReplaceStateInLocation[] = []; + const skipUrlState = isGroupedNavEnabled ? linkInfo?.skipUrlState : isAdministration(type); // Delete all query strings from URL when the page is security/administration (Manage menu group) - if (isAdministration(type)) { + if (skipUrlState) { ALL_URL_STATE_KEYS.forEach((urlKey: KeyUrlState) => { urlStateUpdatesToLocation.push({ urlStateToReplace: '', @@ -146,6 +152,8 @@ export const useUrlStateHooks = ({ setInitialStateFromUrl, urlState, isFirstPageLoad, + isGroupedNavEnabled, + linkInfo?.skipUrlState, ] ); @@ -159,8 +167,9 @@ export const useUrlStateHooks = ({ if (browserPathName !== pathName) return; const type: UrlStateType = getUrlType(pageName); + const skipUrlState = isGroupedNavEnabled ? linkInfo?.skipUrlState : isAdministration(type); - if (!deepEqual(urlState, prevProps.urlState) && !isFirstPageLoad && !isAdministration(type)) { + if (!deepEqual(urlState, prevProps.urlState) && !isFirstPageLoad && !skipUrlState) { const urlStateUpdatesToLocation: ReplaceStateInLocation[] = ALL_URL_STATE_KEYS.map( (urlKey: KeyUrlState) => ({ urlStateToReplace: getUrlStateKeyValue(urlState, urlKey), @@ -186,11 +195,17 @@ export const useUrlStateHooks = ({ browserPathName, handleInitialize, search, + isGroupedNavEnabled, + linkInfo?.skipUrlState, ]); useEffect(() => { - document.title = `${getTitle(pageName, navTabs)} - Kibana`; - }, [pageName, navTabs]); + if (!isGroupedNavEnabled) { + document.title = `${getTitle(pageName, navTabs)} - Kibana`; + } else { + document.title = `${linkInfo?.title ?? ''} - Kibana`; + } + }, [pageName, navTabs, isGroupedNavEnabled, linkInfo]); useEffect(() => { queryTimelineByIdOnUrlChange({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 05a91f094ed38c..efc4666b7bd61e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -13,6 +13,8 @@ import { connect, ConnectedProps } from 'react-redux'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public'; +import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item'; +import { OsqueryFlyout } from '../../osquery/osquery_flyout'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; @@ -63,6 +65,7 @@ const AlertContextMenuComponent: React.FC { const [isPopoverOpen, setPopover] = useState(false); + const [isOsqueryFlyoutOpen, setOsqueryFlyoutOpen] = useState(false); const [routeProps] = useRouteSpy(); const onMenuItemClick = useCallback(() => { @@ -186,18 +189,38 @@ const AlertContextMenuComponent: React.FC get(0, ecsRowData?.agent?.id), [ecsRowData]); + + const handleOnOsqueryClick = useCallback(() => { + setOsqueryFlyoutOpen((prevValue) => !prevValue); + setPopover(false); + }, []); + + const { osqueryActionItems } = useOsqueryContextActionItem({ handleClick: handleOnOsqueryClick }); + const items: React.ReactElement[] = useMemo( () => !isEvent && ruleId - ? [...addToCaseActionItems, ...statusActionItems, ...exceptionActionItems] - : [...addToCaseActionItems, ...eventFilterActionItems], + ? [ + ...addToCaseActionItems, + ...statusActionItems, + ...exceptionActionItems, + ...(agentId ? osqueryActionItems : []), + ] + : [ + ...addToCaseActionItems, + ...eventFilterActionItems, + ...(agentId ? osqueryActionItems : []), + ], [ - statusActionItems, - addToCaseActionItems, - eventFilterActionItems, - exceptionActionItems, isEvent, ruleId, + addToCaseActionItems, + statusActionItems, + exceptionActionItems, + agentId, + osqueryActionItems, + eventFilterActionItems, ] ); @@ -239,6 +262,9 @@ const AlertContextMenuComponent: React.FC )} + {isOsqueryFlyoutOpen && agentId && ecsRowData != null && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx index ca61e2f3ebf6d8..e27a13ef217e3f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx @@ -13,14 +13,12 @@ interface IProps { handleClick: () => void; } -export const OsqueryActionItem = ({ handleClick }: IProps) => { - return ( - - {ACTION_OSQUERY} - - ); -}; +export const OsqueryActionItem = ({ handleClick }: IProps) => ( + + {ACTION_OSQUERY} + +); diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/use_osquery_context_action_item.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/use_osquery_context_action_item.tsx new file mode 100644 index 00000000000000..41a78eb32619f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/use_osquery_context_action_item.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { OsqueryActionItem } from './osquery_action_item'; +import { useKibana } from '../../../common/lib/kibana'; + +interface IProps { + handleClick: () => void; +} + +export const useOsqueryContextActionItem = ({ handleClick }: IProps) => { + const osqueryActionItem = useMemo( + () => , + [handleClick] + ); + const permissions = useKibana().services.application.capabilities.osquery; + + return { + osqueryActionItems: + permissions?.writeLiveQueries || permissions?.runSavedQueries ? [osqueryActionItem] : [], + }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts deleted file mode 100644 index d405837a4f7f20..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts +++ /dev/null @@ -1,29 +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 { getBreadcrumbs } from './utils'; - -const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: boolean }) => - `${appId}${options?.path ?? ''}`; - -describe('getBreadcrumbs', () => { - it('Does not render for incorrect params', () => { - expect( - getBreadcrumbs( - { - pageName: 'pageName', - detailName: 'detailName', - tabName: undefined, - search: '', - pathName: 'pathName', - }, - [], - getUrlForAppMock - ) - ).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index b4778bb8c24eae..21737d307f3fd1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -5,19 +5,14 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; - import { ChromeBreadcrumb } from '@kbn/core/public'; -import { - getRulesUrl, - getRuleDetailsUrl, -} from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { getRuleDetailsUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import * as i18nRules from './translations'; import { RouteSpyState } from '../../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../../common/components/navigation/types'; import { SecurityPageName } from '../../../../app/types'; -import { APP_UI_ID, RULES_PATH } from '../../../../../common/constants'; +import { RULES_PATH } from '../../../../../common/constants'; import { RuleStep, RuleStepsOrder } from './types'; +import { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.defineRule, @@ -26,47 +21,26 @@ export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.ruleActions, ]; -const getRulesBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetUrlForApp) => { - const tabPath = pathname.split('/')[1]; - - if (tabPath === 'rules') { - return { - text: i18nRules.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRulesUrl(!isEmpty(search[0]) ? search[0] : ''), - }), - }; - } -}; - const isRuleCreatePage = (pathname: string) => pathname.includes(RULES_PATH) && pathname.includes('/create'); const isRuleEditPage = (pathname: string) => pathname.includes(RULES_PATH) && pathname.includes('/edit'); -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: RouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { let breadcrumb: ChromeBreadcrumb[] = []; - const rulesBreadcrumb = getRulesBreadcrumb(params.pathName, search, getUrlForApp); - - if (rulesBreadcrumb) { - breadcrumb = [...breadcrumb, rulesBreadcrumb]; - } - if (params.detailName && params.state?.ruleName) { breadcrumb = [ ...breadcrumb, { text: params.state.ruleName, - href: getUrlForApp(APP_UI_ID, { + href: getSecuritySolutionUrl({ deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + path: getRuleDetailsUrl(params.detailName, ''), }), }, ]; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts index 859790b4f342ea..061dba0c373581 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { hostsModel } from '../../store'; @@ -14,9 +14,8 @@ import { getHostDetailsUrl } from '../../../common/components/link_to/redirect_t import * as i18n from '../translations'; import { HostRouteSpyState } from '../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../common/components/navigation/types'; -import { APP_UI_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; +import { GetSecuritySolutionUrl } from '../../../common/components/link_to'; export const type = hostsModel.HostsType.details; @@ -31,28 +30,19 @@ const TabNameMappedToI18nKey: Record = { [HostsTableType.sessions]: i18n.NAVIGATION_SESSIONS_TITLE, }; -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: HostRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - path: !isEmpty(search[0]) ? search[0] : '', - deepLinkId: SecurityPageName.hosts, - }), - }, - ]; + let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { breadcrumb = [ ...breadcrumb, { text: params.detailName, - href: getUrlForApp(APP_UI_ID, { - path: getHostDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + href: getSecuritySolutionUrl({ + path: getHostDetailsUrl(params.detailName, ''), deepLinkId: SecurityPageName.hosts, }), }, diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts index 49b4214d60bd67..2fec83e4239177 100644 --- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -20,7 +20,7 @@ const TabNameMappedToI18nKey: Record = { [AdministrationSubTab.blocklist]: BLOCKLIST, }; -export function getBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] { +export function getTrailingBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] { return [ ...(params?.tabName ? [params?.tabName] : []).map((tabName) => ({ text: TabNameMappedToI18nKey[tabName], diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx index e01ab13722bf26..f28798af68dc2b 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -50,7 +50,7 @@ import { SecurityPageName } from '../../../app/types'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; import { LandingPageComponent } from '../../../common/components/landing_page'; -export { getBreadcrumbs } from './utils'; +export { getTrailingBreadcrumbs } from './utils'; const NetworkDetailsManage = manageQuery(IpOverview); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts index 044c1d22a63488..d0d885fc47a79c 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { decodeIpv6 } from '../../../common/lib/helpers'; @@ -14,9 +14,8 @@ import { networkModel } from '../../store'; import * as i18n from '../translations'; import { NetworkRouteType } from '../navigation/types'; import { NetworkRouteSpyState } from '../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../common/components/navigation/types'; -import { APP_UI_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; +import { GetSecuritySolutionUrl } from '../../../common/components/link_to'; export const type = networkModel.NetworkType.details; const TabNameMappedToI18nKey: Record = { @@ -28,33 +27,19 @@ const TabNameMappedToI18nKey: Record = { [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, }; -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: NetworkRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - deepLinkId: SecurityPageName.network, - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, - ]; + let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { breadcrumb = [ - ...breadcrumb, { text: decodeIpv6(params.detailName), - href: getUrlForApp(APP_UI_ID, { + href: getSecuritySolutionUrl({ deepLinkId: SecurityPageName.network, - path: getNetworkDetailsUrl( - params.detailName, - params.flowTarget, - !isEmpty(search[0]) ? search[0] : '' - ), + path: getNetworkDetailsUrl(params.detailName, params.flowTarget, ''), }), }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index b2c813087f8dba..5ad969adba5cd8 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -5,39 +5,20 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; import React from 'react'; import { Switch, Route, Redirect } from 'react-router-dom'; -import { ChromeBreadcrumb } from '@kbn/core/public'; - import { TimelineType } from '../../../common/types/timeline'; -import { TimelineRouteSpyState } from '../../common/utils/route/types'; import { TimelinesPage } from './timelines_page'; -import { PAGE_TITLE } from './translations'; + import { appendSearch } from '../../common/components/link_to/helpers'; -import { GetUrlForApp } from '../../common/components/navigation/types'; -import { APP_UI_ID, TIMELINES_PATH } from '../../../common/constants'; -import { SecurityPageName } from '../../app/types'; + +import { TIMELINES_PATH } from '../../../common/constants'; const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineType.default}|${TimelineType.template})`; const timelinesDefaultPath = `${TIMELINES_PATH}/${TimelineType.default}`; -export const getBreadcrumbs = ( - params: TimelineRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp -): ChromeBreadcrumb[] => [ - { - text: PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - deepLinkId: SecurityPageName.timelines, - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, -]; - export const Timelines = React.memo(() => ( diff --git a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts index 26ed75997a85df..a9b3cb30ef84a6 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty } from 'lodash/fp'; +import { get } from 'lodash/fp'; import { ChromeBreadcrumb } from '@kbn/core/public'; import { usersModel } from '../../store'; @@ -14,9 +14,8 @@ import { getUsersDetailsUrl } from '../../../common/components/link_to/redirect_ import * as i18n from '../translations'; import { UsersRouteSpyState } from '../../../common/utils/route/types'; -import { GetUrlForApp } from '../../../common/components/navigation/types'; -import { APP_UI_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; +import { GetSecuritySolutionUrl } from '../../../common/components/link_to'; export const type = usersModel.UsersType.details; @@ -30,28 +29,18 @@ const TabNameMappedToI18nKey: Record = { [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, }; -export const getBreadcrumbs = ( +export const getTrailingBreadcrumbs = ( params: UsersRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp + getSecuritySolutionUrl: GetSecuritySolutionUrl ): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getUrlForApp(APP_UI_ID, { - path: !isEmpty(search[0]) ? search[0] : '', - deepLinkId: SecurityPageName.users, - }), - }, - ]; + let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { breadcrumb = [ - ...breadcrumb, { text: params.detailName, - href: getUrlForApp(APP_UI_ID, { - path: getUsersDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + href: getSecuritySolutionUrl({ + path: getUsersDetailsUrl(params.detailName, ''), deepLinkId: SecurityPageName.users, }), }, diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts index 192fcf06c30951..3ddf0cebd0cf30 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts @@ -40,12 +40,12 @@ journey('MonitorDetails', async ({ page, params }: { page: Page; params: any }) step('create basic monitor', async () => { await uptime.enableMonitorManagement(); await uptime.clickAddMonitor(); - await uptime.createBasicMonitorDetails({ + await uptime.createBasicHTTPMonitorDetails({ name, locations: ['US Central'], apmServiceName: 'synthetics', + url: 'https://www.google.com', }); - await uptime.fillByTestSubj('syntheticsUrlField', 'https://www.google.com'); await uptime.confirmAndSave(); }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts index a21627548aeb1f..a9dd2c46334023 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts @@ -21,12 +21,12 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); const createBasicMonitor = async () => { - await uptime.createBasicMonitorDetails({ + await uptime.createBasicHTTPMonitorDetails({ name, locations: ['US Central'], apmServiceName: 'synthetics', + url: 'https://www.google.com', }); - await uptime.fillByTestSubj('syntheticsUrlField', 'https://www.google.com'); }; before(async () => { @@ -52,12 +52,12 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => step(`shows error if name already exists`, async () => { await uptime.navigateToAddMonitor(); - await uptime.createBasicMonitorDetails({ + await uptime.createBasicHTTPMonitorDetails({ name, locations: ['US Central'], apmServiceName: 'synthetics', + url: 'https://www.google.com', }); - await uptime.fillByTestSubj('syntheticsUrlField', 'https://www.google.com'); await uptime.assertText({ text: 'Monitor name already exists.' }); diff --git a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx index eb13c3678f47e6..91d8151c297013 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx @@ -189,6 +189,7 @@ export function monitorManagementPageProvider({ apmServiceName: string; locations: string[]; }) { + await this.selectMonitorType('http'); await this.createBasicMonitorDetails({ name, apmServiceName, locations }); await this.fillByTestSubj('syntheticsUrlField', url); }, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx index a9cdc2c78d86de..99419e2ca9145b 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/contexts/policy_config_context.tsx @@ -121,10 +121,10 @@ export function PolicyConfigContextProvider({ const isAddMonitorRoute = useRouteMatch(MONITOR_ADD_ROUTE); useEffect(() => { - if (isAddMonitorRoute) { + if (isAddMonitorRoute?.isExact) { setMonitorType(DataStream.BROWSER); } - }, [isAddMonitorRoute]); + }, [isAddMonitorRoute?.isExact]); const value = useMemo(() => { return { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts index c30d9af766b48c..48d052d35a1f8b 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts @@ -53,6 +53,7 @@ describe('formatMonitorConfig', () => { expect(yamlConfig).toEqual({ 'check.request.method': 'GET', enabled: true, + locations: [], max_redirects: '0', name: 'Test', password: '3z9SBOQWW5F0UrdqLVFqlF6z', @@ -110,6 +111,7 @@ describe('formatMonitorConfig', () => { 'filter_journeys.tags': ['dev'], ignore_https_errors: false, name: 'Test', + locations: [], schedule: '@every 3m', screenshots: 'on', 'source.inline.script': diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts index e2a1bf1b869edf..ea298992d22465 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts @@ -15,7 +15,6 @@ const UI_KEYS_TO_SKIP = [ ConfigKey.DOWNLOAD_SPEED, ConfigKey.LATENCY, ConfigKey.IS_THROTTLING_ENABLED, - ConfigKey.LOCATIONS, ConfigKey.REVISION, 'secrets', ]; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts index 305f1d15a48238..952e18ce9c884c 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts @@ -5,10 +5,13 @@ * 2.0. */ -import { SyntheticsService } from './synthetics_service'; +jest.mock('axios', () => jest.fn()); + +import { SyntheticsService, SyntheticsConfig } from './synthetics_service'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { loggerMock } from '@kbn/core/server/logging/logger.mock'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; +import axios, { AxiosResponse } from 'axios'; describe('SyntheticsService', () => { const mockEsClient = { @@ -67,4 +70,71 @@ describe('SyntheticsService', () => { }, ]); }); + + describe('addConfig', () => { + afterEach(() => jest.restoreAllMocks()); + + it('saves configs only to the selected locations', async () => { + serverMock.config = { service: { devUrl: 'http://localhost' } }; + const service = new SyntheticsService(logger, serverMock, { + username: 'dev', + password: '12345', + }); + + service.apiClient.locations = [ + { + id: 'selected', + label: 'Selected Location', + url: 'example.com/1', + geo: { + lat: 0, + lon: 0, + }, + isServiceManaged: true, + }, + { + id: 'not selected', + label: 'Not Selected Location', + url: 'example.com/2', + geo: { + lat: 0, + lon: 0, + }, + isServiceManaged: true, + }, + ]; + + jest.spyOn(service, 'getApiKey').mockResolvedValue({ name: 'example', id: 'i', apiKey: 'k' }); + jest.spyOn(service, 'getOutput').mockResolvedValue({ hosts: ['es'], api_key: 'i:k' }); + + const payload = { + type: 'http', + enabled: true, + schedule: { + number: '3', + unit: 'm', + }, + name: 'my mon', + locations: [{ id: 'selected', isServiceManaged: true }], + urls: 'http://google.com', + max_redirects: '0', + password: '', + proxy_url: '', + id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', + fields: { config_id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d' }, + fields_under_root: true, + }; + + (axios as jest.MockedFunction).mockResolvedValue({} as AxiosResponse); + + await service.addConfig(payload as SyntheticsConfig); + + expect(axios).toHaveBeenCalledTimes(1); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'example.com/1/monitors', + }) + ); + }); + }); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index f655dd6d4cc8cb..b1af1717e1a1cb 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -48,7 +48,7 @@ const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE = const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID = 'UPTIME:SyntheticsService:sync-task'; const SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT = '5m'; -type SyntheticsConfig = SyntheticsMonitorWithId & { +export type SyntheticsConfig = SyntheticsMonitorWithId & { fields_under_root?: boolean; fields?: { config_id: string; run_once?: boolean; test_run_id?: string }; }; @@ -56,7 +56,7 @@ type SyntheticsConfig = SyntheticsMonitorWithId & { export class SyntheticsService { private logger: Logger; private readonly server: UptimeServerSetup; - private apiClient: ServiceAPIClient; + public apiClient: ServiceAPIClient; private readonly config: ServiceConfig; private readonly esHosts: string[]; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts index 5c6a0ac0bd4162..10a4fae0a036d1 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts @@ -208,6 +208,9 @@ describe('Search Strategy EQL helper', () => { "_id": "qhymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.security-default-2021.02.05-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], @@ -335,6 +338,9 @@ describe('Search Strategy EQL helper', () => { "_id": "qxymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.security-default-2021.02.05-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], @@ -476,6 +482,9 @@ describe('Search Strategy EQL helper', () => { "_id": "rBymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.security-default-2021.02.05-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], @@ -592,6 +601,9 @@ describe('Search Strategy EQL helper', () => { "_id": "pxymg3cBX5UUcOOYP3Ec", "_index": ".ds-logs-endpoint.events.process-default-2021.02.02-000005", "agent": Object { + "id": Array [ + "1d15cf9e-3dc7-5b97-f586-743f7c2518b2", + ], "type": Array [ "endpoint", ], diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts index 211edec96b8acd..068b52b8cd821b 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts @@ -94,6 +94,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'event.timezone', 'event.type', 'agent.type', + 'agent.id', 'auditd.result', 'auditd.session', 'auditd.data.acct', diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts new file mode 100644 index 00000000000000..f0329a220c71a3 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { timerange, observer } from '@elastic/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace'; + +export async function addAgentConfigMetrics({ + synthtraceEsClient, + start, + end, + etag, +}: { + synthtraceEsClient: ApmSynthtraceEsClient; + start: number; + end: number; + etag?: string; +}) { + const agentConfig = observer().agentConfig(); + + const agentConfigEvents = [ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => agentConfig.etag(etag ?? 'test-etag').timestamp(timestamp)), + ]; + + await synthtraceEsClient.index(agentConfigEvents); +} diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.spec.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts similarity index 85% rename from x-pack/test/apm_api_integration/tests/settings/agent_configuration.spec.ts rename to x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts index ecf5b87e82d70b..e4960791eee5ac 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts @@ -11,14 +11,17 @@ import expect from '@kbn/expect'; import { omit, orderBy } from 'lodash'; import { AgentConfigurationIntake } from '@kbn/apm-plugin/common/agent_configuration/configuration_types'; import { AgentConfigSearchParams } from '@kbn/apm-plugin/server/routes/settings/agent_configuration/route'; - -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import moment from 'moment'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { addAgentConfigMetrics } from './add_agent_config_metrics'; export default function agentConfigurationTests({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const log = getService('log'); + const synthtraceEsClient = getService('synthtraceEsClient'); const archiveName = 'apm_8.0.0'; @@ -77,6 +80,18 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); } + function findExactConfiguration(name: string, environment: string) { + return apmApiClient.readUser({ + endpoint: 'GET /api/apm/settings/agent-configuration/view', + params: { + query: { + name, + environment, + }, + }, + }); + } + registry.when( 'agent configuration when no data is loaded', { config: 'basic', archives: [] }, @@ -297,7 +312,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'production' }, settings: { transaction_sample_rate: '0.9' }, }; - let etag: string | undefined; + let etag: string; before(async () => { log.debug('creating agent configuration'); @@ -371,6 +386,74 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte } ); + registry.when( + 'Agent configurations through fleet', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + const name = 'myservice'; + const environment = 'development'; + const testConfig = { + service: { name, environment }, + settings: { transaction_sample_rate: '0.9' }, + }; + + let agentConfiguration: + | APIReturnType<'GET /api/apm/settings/agent-configuration/view'> + | undefined; + + before(async () => { + log.debug('creating agent configuration'); + await createConfiguration(testConfig); + const { body } = await findExactConfiguration(name, environment); + agentConfiguration = body; + }); + + after(async () => { + await deleteConfiguration(testConfig); + }); + + it(`should have 'applied_by_agent=false' when there are no agent config metrics for this etag`, async () => { + expect(agentConfiguration?.applied_by_agent).to.be(false); + }); + + describe('when there are agent config metrics for this etag', () => { + before(async () => { + const start = new Date().getTime(); + const end = moment(start).add(15, 'minutes').valueOf(); + + await addAgentConfigMetrics({ + synthtraceEsClient, + start, + end, + etag: agentConfiguration?.etag, + }); + }); + + after(() => synthtraceEsClient.clean()); + + it(`should have 'applied_by_agent=true' when getting a config from all configurations`, async () => { + const { + body: { configurations }, + } = await getAllConfigurations(); + + const updatedConfig = configurations.find( + (x) => x.service.name === name && x.service.environment === environment + ); + + expect(updatedConfig?.applied_by_agent).to.be(true); + }); + + it(`should have 'applied_by_agent=true' when getting a single config`, async () => { + const { + body: { applied_by_agent: appliedByAgent }, + } = await findExactConfiguration(name, environment); + + expect(appliedByAgent).to.be(true); + }); + }); + } + ); + registry.when( 'agent configuration when data is loaded', { config: 'basic', archives: [archiveName] },