diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index dcceca784891..945f85a82097 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -15,7 +15,7 @@ const STORYBOOKS = [ 'apm', 'canvas', 'ci_composite', - 'cloud', + 'cloud_chat', 'coloring', 'chart_icons', 'controls', diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc index 43c7f4cde8fa..275bd1c21f9e 100644 --- a/docs/api/saved-objects/find.asciidoc +++ b/docs/api/saved-objects/find.asciidoc @@ -52,6 +52,15 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit `has_reference`:: (Optional, object) Filters to objects that have a relationship with the type and ID combination. +`has_reference_operator`:: + (Optional, string) The operator to use for the `has_reference` parameter. Either `OR` or `AND`. Defaults to `OR`. + +`has_no_reference`:: + (Optional, object) Filters to objects that do not have a relationship with the type and ID combination. + +`has_no_reference_operator`:: + (Optional, string) The operator to use for the `has_no_reference` parameter. Either `OR` or `AND`. Defaults to `OR`. + `filter`:: (Optional, string) The filter is a KQL string with the caveat that if you filter with an attribute from your saved object type, it should look like that: `savedObjectType.attributes.title: "myTitle"`. However, If you use a root attribute of a saved diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index f4fc9c67508e..407261c6f1d7 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -424,10 +424,22 @@ The plugin exposes the static DefaultEditorController class to consume. |The cloud plugin adds Cloud-specific features to Kibana. +|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_chat/README.md[cloudChat] +|Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud. + + |{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_experiments/README.mdx[cloudExperiments] |The Cloud Experiments Service provides the necessary APIs to implement A/B testing scenarios, fetching the variations in configuration and reporting back metrics to track conversion rates of the experiments. +|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_full_story/README.md[cloudFullStory] +|Integrates with FullStory in order to provide better product analytics, so we can understand how our users make use of Kibana. This plugin should only run on Elastic Cloud. + + +|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_links/README.md[cloudLinks] +|Adds all the links to the Elastic Cloud console. + + |{kib-repo}blob/{branch}/x-pack/plugins/cloud_security_posture/README.md[cloudSecurityPosture] |Cloud Posture automates the identification and remediation of risks across cloud infrastructures diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts index 0739c9acab8f..d9a65f984c22 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts @@ -4182,6 +4182,10 @@ describe('SavedObjectsRepository', () => { type: 'foo', id: '1', }, + hasNoReference: { + type: 'bar', + id: '1', + }, }; it(`passes mappings, registry, and search options to getSearchDsl`, async () => { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 5569141c7fa0..f48e031bd23c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -1129,6 +1129,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { rootSearchFields, hasReference, hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, page = FIND_DEFAULT_PAGE, perPage = FIND_DEFAULT_PER_PAGE, pit, @@ -1235,6 +1237,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { typeToNamespacesMap, hasReference, hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, kueryNode, }), }, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts index c502665468e6..20ce3a2f46b2 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts @@ -195,17 +195,18 @@ describe('#getQueryParams', () => { }); }); - describe('reference filter clause', () => { - describe('`hasReference` parameter', () => { - it('does not call `getReferencesFilter` when `hasReference` is not specified', () => { - getQueryParams({ - registry, - hasReference: undefined, - }); - - expect(getReferencesFilterMock).not.toHaveBeenCalled(); + describe('reference/noreference filter clause', () => { + it('does not call `getReferencesFilter` when neither `hasReference` nor `hasNoReference` are specified', () => { + getQueryParams({ + registry, + hasReference: undefined, + hasNoReference: undefined, }); + expect(getReferencesFilterMock).not.toHaveBeenCalled(); + }); + + describe('`hasReference` parameter', () => { it('calls `getReferencesFilter` with the correct parameters', () => { const hasReference = { id: 'foo', type: 'bar' }; getQueryParams({ @@ -235,6 +236,38 @@ describe('#getQueryParams', () => { expect(filters.some((filter) => filter.references_filter === true)).toBeDefined(); }); }); + + describe('`hasNoReference` parameter', () => { + it('calls `getReferencesFilter` with the correct parameters', () => { + const hasNoReference = { id: 'noFoo', type: 'bar' }; + getQueryParams({ + registry, + hasNoReference, + hasNoReferenceOperator: 'AND', + }); + + expect(getReferencesFilterMock).toHaveBeenCalledTimes(1); + expect(getReferencesFilterMock).toHaveBeenCalledWith({ + must: false, + references: [hasNoReference], + operator: 'AND', + }); + }); + + it('includes the return of `getReferencesFilter` in the `filter` clause', () => { + getReferencesFilterMock.mockReturnValue({ references_filter: true }); + + const hasNoReference = { id: 'noFoo', type: 'bar' }; + const result = getQueryParams({ + registry, + hasNoReference, + hasReferenceOperator: 'AND', + }); + + const filters: any[] = result.query.bool.filter; + expect(filters.some((filter) => filter.references_filter === true)).toBeDefined(); + }); + }); }); describe('type filter clauses', () => { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts index 669f2a273569..896b934c90b8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts @@ -7,6 +7,7 @@ */ import * as esKuery from '@kbn/es-query'; +import type { SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common'; type KueryNode = any; @@ -123,11 +124,6 @@ function getClauseForType( }; } -export interface HasReferenceQueryParams { - type: string; - id: string; -} - export type SearchOperator = 'AND' | 'OR'; interface QueryParams { @@ -139,8 +135,10 @@ interface QueryParams { defaultSearchOperator?: SearchOperator; searchFields?: string[]; rootSearchFields?: string[]; - hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[]; + hasReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[]; hasReferenceOperator?: SearchOperator; + hasNoReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[]; + hasNoReferenceOperator?: SearchOperator; kueryNode?: KueryNode; } @@ -148,6 +146,13 @@ interface QueryParams { const uniqNamespaces = (namespacesToNormalize?: string[]) => namespacesToNormalize ? Array.from(new Set(namespacesToNormalize)) : undefined; +const toArray = (val: unknown) => { + if (typeof val === 'undefined') { + return val; + } + return !Array.isArray(val) ? [val] : val; +}; + /** * Get the "query" related keys for the search body */ @@ -162,6 +167,8 @@ export function getQueryParams({ defaultSearchOperator, hasReference, hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, kueryNode, }: QueryParams) { const types = getTypes( @@ -169,9 +176,8 @@ export function getQueryParams({ typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type ); - if (hasReference && !Array.isArray(hasReference)) { - hasReference = [hasReference]; - } + hasReference = toArray(hasReference); + hasNoReference = toArray(hasNoReference); const bool: any = { filter: [ @@ -184,6 +190,15 @@ export function getQueryParams({ }), ] : []), + ...(hasNoReference?.length + ? [ + getReferencesFilter({ + references: hasNoReference, + operator: hasNoReferenceOperator, + must: false, + }), + ] + : []), { bool: { should: types.map((shouldType) => { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts index 9a042579c8e8..127f3a94edd2 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts @@ -20,29 +20,141 @@ describe('getReferencesFilter', () => { }, }); - describe('when using the `OR` operator', () => { - it('generates one `should` clause per type of reference', () => { + describe('for "must" match clauses', () => { + describe('when using the `OR` operator', () => { + it('generates one `should` clause per type of reference', () => { + const references = [ + { type: 'foo', id: 'foo-1' }, + { type: 'foo', id: 'foo-2' }, + { type: 'foo', id: 'foo-3' }, + { type: 'bar', id: 'bar-1' }, + { type: 'bar', id: 'bar-2' }, + ]; + const clause = getReferencesFilter({ + references, + operator: 'OR', + }); + + expect(clause).toEqual({ + bool: { + should: [ + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-1', 'foo-2', 'foo-3'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['bar-1', 'bar-2'] } }, + { term: { 'references.type': 'bar' } }, + ]), + ], + minimum_should_match: 1, + }, + }); + }); + + it('does not include more than `maxTermsPerClause` per `terms` clauses', () => { + const references = [ + { type: 'foo', id: 'foo-1' }, + { type: 'foo', id: 'foo-2' }, + { type: 'foo', id: 'foo-3' }, + { type: 'foo', id: 'foo-4' }, + { type: 'foo', id: 'foo-5' }, + { type: 'bar', id: 'bar-1' }, + { type: 'bar', id: 'bar-2' }, + { type: 'bar', id: 'bar-3' }, + { type: 'dolly', id: 'dolly-1' }, + ]; + const clause = getReferencesFilter({ + references, + operator: 'OR', + maxTermsPerClause: 2, + }); + + expect(clause).toEqual({ + bool: { + should: [ + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-1', 'foo-2'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-3', 'foo-4'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-5'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['bar-1', 'bar-2'] } }, + { term: { 'references.type': 'bar' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['bar-3'] } }, + { term: { 'references.type': 'bar' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['dolly-1'] } }, + { term: { 'references.type': 'dolly' } }, + ]), + ], + minimum_should_match: 1, + }, + }); + }); + }); + + describe('when using the `AND` operator', () => { + it('generates one `must` clause per reference', () => { + const references = [ + { type: 'foo', id: 'foo-1' }, + { type: 'foo', id: 'foo-2' }, + { type: 'bar', id: 'bar-1' }, + ]; + + const clause = getReferencesFilter({ + references, + operator: 'AND', + }); + + expect(clause).toEqual({ + bool: { + must: references.map((ref) => ({ + nested: { + path: 'references', + query: { + bool: { + must: [ + { term: { 'references.id': ref.id } }, + { term: { 'references.type': ref.type } }, + ], + }, + }, + }, + })), + }, + }); + }); + }); + + it('defaults to using the `OR` operator', () => { const references = [ { type: 'foo', id: 'foo-1' }, - { type: 'foo', id: 'foo-2' }, - { type: 'foo', id: 'foo-3' }, { type: 'bar', id: 'bar-1' }, - { type: 'bar', id: 'bar-2' }, ]; const clause = getReferencesFilter({ references, - operator: 'OR', }); expect(clause).toEqual({ bool: { should: [ nestedRefMustClauses([ - { terms: { 'references.id': ['foo-1', 'foo-2', 'foo-3'] } }, + { terms: { 'references.id': ['foo-1'] } }, { term: { 'references.type': 'foo' } }, ]), nestedRefMustClauses([ - { terms: { 'references.id': ['bar-1', 'bar-2'] } }, + { terms: { 'references.id': ['bar-1'] } }, { term: { 'references.type': 'bar' } }, ]), ], @@ -50,115 +162,156 @@ describe('getReferencesFilter', () => { }, }); }); + }); + + describe('for "must_not" match clauses', () => { + describe('when using the `OR` operator', () => { + it('generates one `must_not` clause per type of reference', () => { + const references = [ + { type: 'foo', id: 'foo-1' }, + { type: 'foo', id: 'foo-2' }, + { type: 'foo', id: 'foo-3' }, + { type: 'bar', id: 'bar-1' }, + { type: 'bar', id: 'bar-2' }, + ]; + const clause = getReferencesFilter({ + references, + operator: 'OR', + must: false, + }); + + expect(clause).toEqual({ + bool: { + must_not: [ + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-1', 'foo-2', 'foo-3'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['bar-1', 'bar-2'] } }, + { term: { 'references.type': 'bar' } }, + ]), + ], + }, + }); + }); + + it('does not include more than `maxTermsPerClause` per `terms` clauses', () => { + const references = [ + { type: 'foo', id: 'foo-1' }, + { type: 'foo', id: 'foo-2' }, + { type: 'foo', id: 'foo-3' }, + { type: 'foo', id: 'foo-4' }, + { type: 'foo', id: 'foo-5' }, + { type: 'bar', id: 'bar-1' }, + { type: 'bar', id: 'bar-2' }, + { type: 'bar', id: 'bar-3' }, + { type: 'dolly', id: 'dolly-1' }, + ]; + const clause = getReferencesFilter({ + references, + operator: 'OR', + maxTermsPerClause: 2, + must: false, + }); + + expect(clause).toEqual({ + bool: { + must_not: [ + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-1', 'foo-2'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-3', 'foo-4'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['foo-5'] } }, + { term: { 'references.type': 'foo' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['bar-1', 'bar-2'] } }, + { term: { 'references.type': 'bar' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['bar-3'] } }, + { term: { 'references.type': 'bar' } }, + ]), + nestedRefMustClauses([ + { terms: { 'references.id': ['dolly-1'] } }, + { term: { 'references.type': 'dolly' } }, + ]), + ], + }, + }); + }); + }); - it('does not include mode than `maxTermsPerClause` per `terms` clauses', () => { + describe('when using the `AND` operator', () => { + it('generates one `must` clause per reference', () => { + const references = [ + { type: 'foo', id: 'foo-1' }, + { type: 'foo', id: 'foo-2' }, + { type: 'bar', id: 'bar-1' }, + ]; + + const clause = getReferencesFilter({ + references, + operator: 'AND', + must: false, + }); + + expect(clause).toEqual({ + bool: { + must_not: [ + { + bool: { + must: references.map((ref) => ({ + nested: { + path: 'references', + query: { + bool: { + must: [ + { term: { 'references.id': ref.id } }, + { term: { 'references.type': ref.type } }, + ], + }, + }, + }, + })), + }, + }, + ], + }, + }); + }); + }); + + it('defaults to using the `OR` operator', () => { const references = [ { type: 'foo', id: 'foo-1' }, - { type: 'foo', id: 'foo-2' }, - { type: 'foo', id: 'foo-3' }, - { type: 'foo', id: 'foo-4' }, - { type: 'foo', id: 'foo-5' }, { type: 'bar', id: 'bar-1' }, - { type: 'bar', id: 'bar-2' }, - { type: 'bar', id: 'bar-3' }, - { type: 'dolly', id: 'dolly-1' }, ]; const clause = getReferencesFilter({ references, - operator: 'OR', - maxTermsPerClause: 2, + must: false, }); expect(clause).toEqual({ bool: { - should: [ - nestedRefMustClauses([ - { terms: { 'references.id': ['foo-1', 'foo-2'] } }, - { term: { 'references.type': 'foo' } }, - ]), + must_not: [ nestedRefMustClauses([ - { terms: { 'references.id': ['foo-3', 'foo-4'] } }, + { terms: { 'references.id': ['foo-1'] } }, { term: { 'references.type': 'foo' } }, ]), nestedRefMustClauses([ - { terms: { 'references.id': ['foo-5'] } }, - { term: { 'references.type': 'foo' } }, - ]), - nestedRefMustClauses([ - { terms: { 'references.id': ['bar-1', 'bar-2'] } }, + { terms: { 'references.id': ['bar-1'] } }, { term: { 'references.type': 'bar' } }, ]), - nestedRefMustClauses([ - { terms: { 'references.id': ['bar-3'] } }, - { term: { 'references.type': 'bar' } }, - ]), - nestedRefMustClauses([ - { terms: { 'references.id': ['dolly-1'] } }, - { term: { 'references.type': 'dolly' } }, - ]), ], - minimum_should_match: 1, - }, - }); - }); - }); - - describe('when using the `AND` operator', () => { - it('generates one `must` clause per reference', () => { - const references = [ - { type: 'foo', id: 'foo-1' }, - { type: 'foo', id: 'foo-2' }, - { type: 'bar', id: 'bar-1' }, - ]; - - const clause = getReferencesFilter({ - references, - operator: 'AND', - }); - - expect(clause).toEqual({ - bool: { - must: references.map((ref) => ({ - nested: { - path: 'references', - query: { - bool: { - must: [ - { term: { 'references.id': ref.id } }, - { term: { 'references.type': ref.type } }, - ], - }, - }, - }, - })), }, }); }); }); - - it('defaults to using the `OR` operator', () => { - const references = [ - { type: 'foo', id: 'foo-1' }, - { type: 'bar', id: 'bar-1' }, - ]; - const clause = getReferencesFilter({ - references, - }); - - expect(clause).toEqual({ - bool: { - should: [ - nestedRefMustClauses([ - { terms: { 'references.id': ['foo-1'] } }, - { term: { 'references.type': 'foo' } }, - ]), - nestedRefMustClauses([ - { terms: { 'references.id': ['bar-1'] } }, - { term: { 'references.type': 'bar' } }, - ]), - ], - minimum_should_match: 1, - }, - }); - }); }); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts index b0849560d2e4..4dd6bc640f17 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts @@ -6,35 +6,61 @@ * Side Public License, v 1. */ -import type { HasReferenceQueryParams, SearchOperator } from './query_params'; +import type { SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common'; + +import type { SearchOperator } from './query_params'; export function getReferencesFilter({ references, operator = 'OR', maxTermsPerClause = 1000, + must = true, }: { - references: HasReferenceQueryParams[]; + references: SavedObjectTypeIdTuple[]; operator?: SearchOperator; maxTermsPerClause?: number; + must?: boolean; }) { if (operator === 'AND') { + if (must) { + return { + bool: { + must: references.map(getNestedTermClauseForReference), + }, + }; + } + return { bool: { - must: references.map(getNestedTermClauseForReference), + must_not: [ + { + bool: { + must: references.map(getNestedTermClauseForReference), + }, + }, + ], }, }; } else { + if (must) { + return { + bool: { + should: getAggregatedTermsClauses(references, maxTermsPerClause), + minimum_should_match: 1, + }, + }; + } + return { bool: { - should: getAggregatedTermsClauses(references, maxTermsPerClause), - minimum_should_match: 1, + must_not: getAggregatedTermsClauses(references, maxTermsPerClause), }, }; } } const getAggregatedTermsClauses = ( - references: HasReferenceQueryParams[], + references: SavedObjectTypeIdTuple[], maxTermsPerClause: number ) => { const refTypeToIds = references.reduce((map, { type, id }) => { @@ -58,7 +84,7 @@ const createChunks = (array: T[], chunkSize: number): T[][] => { return chunks; }; -export const getNestedTermClauseForReference = (reference: HasReferenceQueryParams) => { +export const getNestedTermClauseForReference = (reference: SavedObjectTypeIdTuple) => { return { nested: { path: 'references', diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts index d1ed7251b241..84ef7c232d77 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts @@ -49,7 +49,7 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference, hasReferenceOperator) to getQueryParams', () => { + it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference, hasReferenceOperator, hasNoReference, hasNoReferenceOperator) to getQueryParams', () => { const opts = { namespaces: ['foo-namespace'], type: 'foo', @@ -63,6 +63,11 @@ describe('getSearchDsl', () => { id: '1', }, hasReferenceOperator: 'AND' as queryParamsNS.SearchOperator, + hasNoReference: { + type: 'noBar', + id: '1', + }, + hasNoReferenceOperator: 'AND' as queryParamsNS.SearchOperator, }; getSearchDsl(mappings, registry, opts); @@ -78,6 +83,8 @@ describe('getSearchDsl', () => { defaultSearchOperator: opts.defaultSearchOperator, hasReference: opts.hasReference, hasReferenceOperator: opts.hasReferenceOperator, + hasNoReference: opts.hasNoReference, + hasNoReferenceOperator: opts.hasNoReferenceOperator, }); }); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts index 980bf800755b..381f20069d25 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts @@ -12,7 +12,8 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { SavedObjectsPitParams } from '@kbn/core-saved-objects-api-server'; import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server'; import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; -import { getQueryParams, HasReferenceQueryParams, SearchOperator } from './query_params'; +import type { SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common'; +import { getQueryParams, SearchOperator } from './query_params'; import { getPitParams } from './pit_params'; import { getSortingParams } from './sorting_params'; @@ -30,8 +31,10 @@ interface GetSearchDslOptions { namespaces?: string[]; pit?: SavedObjectsPitParams; typeToNamespacesMap?: Map; - hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[]; + hasReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[]; hasReferenceOperator?: SearchOperator; + hasNoReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[]; + hasNoReferenceOperator?: SearchOperator; kueryNode?: KueryNode; } @@ -54,6 +57,8 @@ export function getSearchDsl( typeToNamespacesMap, hasReference, hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, kueryNode, } = options; @@ -77,6 +82,8 @@ export function getSearchDsl( defaultSearchOperator, hasReference, hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, kueryNode, }), ...getSortingParams(mappings, type, sortField, sortOrder), diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts index 49042029f334..a50506c96c8e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts @@ -66,11 +66,23 @@ export interface SavedObjectsFindOptions { * Use `hasReferenceOperator` to specify the operator to use when searching for multiple references. */ hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[]; + /** * The operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR` */ hasReferenceOperator?: 'AND' | 'OR'; + /** + * Search for documents *not* having a reference to the specified objects. + * Use `hasNoReferenceOperator` to specify the operator to use when searching for multiple references. + */ + hasNoReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[]; + + /** + * The operator to use when searching by multiple references using the `hasNoReference` option. Defaults to `OR` + */ + hasNoReferenceOperator?: 'AND' | 'OR'; + /** * The search operator to use with the provided filter. Defaults to `OR` */ diff --git a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts index 6c2966ee9775..7825b09cf29b 100644 --- a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts +++ b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts @@ -612,6 +612,7 @@ describe('SavedObjectsClient', () => { defaultSearchOperator: 'OR' as const, fields: ['title'], hasReference: { id: '1', type: 'reference' }, + hasNoReference: { id: '1', type: 'reference' }, page: 10, perPage: 100, search: 'what is the meaning of life?|life', @@ -633,6 +634,7 @@ describe('SavedObjectsClient', () => { "fields": Array [ "title", ], + "has_no_reference": "{\\"id\\":\\"1\\",\\"type\\":\\"reference\\"}", "has_reference": "{\\"id\\":\\"1\\",\\"type\\":\\"reference\\"}", "page": 10, "per_page": 100, diff --git a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts index dd2feed58123..1fd111186f55 100644 --- a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts +++ b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts @@ -292,6 +292,8 @@ export class SavedObjectsClient implements SavedObjectsClientContract { fields: 'fields', hasReference: 'has_reference', hasReferenceOperator: 'has_reference_operator', + hasNoReference: 'has_no_reference', + hasNoReferenceOperator: 'has_no_reference_operator', page: 'page', perPage: 'per_page', search: 'search', @@ -315,6 +317,9 @@ export class SavedObjectsClient implements SavedObjectsClientContract { if (query.has_reference) { query.has_reference = JSON.stringify(query.has_reference); } + if (query.has_no_reference) { + query.has_no_reference = JSON.stringify(query.has_no_reference); + } // `aggs` is a structured object. we need to stringify it before sending it, as `fetch` // is not doing it implicitly. diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts index 4587cb1ebeb0..983b31caf7a2 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts @@ -45,6 +45,10 @@ export const registerFindRoute = ( schema.oneOf([referenceSchema, schema.arrayOf(referenceSchema)]) ), has_reference_operator: searchOperatorSchema, + has_no_reference: schema.maybe( + schema.oneOf([referenceSchema, schema.arrayOf(referenceSchema)]) + ), + has_no_reference_operator: searchOperatorSchema, fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), filter: schema.maybe(schema.string()), aggs: schema.maybe(schema.string()), @@ -88,6 +92,8 @@ export const registerFindRoute = ( sortField: query.sort_field, hasReference: query.has_reference, hasReferenceOperator: query.has_reference_operator, + hasNoReference: query.has_no_reference, + hasNoReferenceOperator: query.has_no_reference_operator, fields: typeof query.fields === 'string' ? [query.fields] : query.fields, filter: query.filter, aggs, diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 8ef5a68a3f98..445bf9458d45 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -130,6 +130,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { crawlerGettingStarted: `${ENTERPRISE_SEARCH_DOCS}crawler-getting-started.html`, crawlerManaging: `${ENTERPRISE_SEARCH_DOCS}crawler-managing.html`, crawlerOverview: `${ENTERPRISE_SEARCH_DOCS}crawler.html`, + deployTrainedModels: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-models.html`, documentLevelSecurity: `${ELASTICSEARCH_DOCS}document-level-security.html`, ingestPipelines: `${ENTERPRISE_SEARCH_DOCS}ingest-pipelines.html`, languageAnalyzers: `${ELASTICSEARCH_DOCS}analysis-lang-analyzer.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index aed1b552bdb3..d9902a7b11de 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -115,6 +115,7 @@ export interface DocLinks { readonly crawlerGettingStarted: string; readonly crawlerManaging: string; readonly crawlerOverview: string; + readonly deployTrainedModels: string; readonly documentLevelSecurity: string; readonly ingestPipelines: string; readonly languageAnalyzers: string; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 5cd145802862..67064af8cddc 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -10,7 +10,10 @@ pageLoadAssetSize: cases: 144442 charts: 55000 cloud: 21076 + cloudChat: 19894 cloudExperiments: 59358 + cloudFullStory: 18493 + cloudLinks: 17629 cloudSecurityPosture: 19109 console: 46091 controls: 40000 diff --git a/src/core/server/integration_tests/saved_objects/routes/find.test.ts b/src/core/server/integration_tests/saved_objects/routes/find.test.ts index ab3ca6c459da..2c7b1c9838b5 100644 --- a/src/core/server/integration_tests/saved_objects/routes/find.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/find.test.ts @@ -123,6 +123,7 @@ describe('GET /api/saved_objects/_find', () => { type: ['foo', 'bar'], defaultSearchOperator: 'OR', hasReferenceOperator: 'OR', + hasNoReferenceOperator: 'OR', }); }); @@ -213,6 +214,73 @@ describe('GET /api/saved_objects/_find', () => { ); }); + it('accepts the query parameter has_no_reference as an object', async () => { + const references = querystring.escape( + JSON.stringify({ + id: '1', + type: 'reference', + }) + ); + await supertest(httpSetup.server.listener) + .get(`/api/saved_objects/_find?type=foo&has_no_reference=${references}`) + .expect(200); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + + const options = savedObjectsClient.find.mock.calls[0][0]; + expect(options.hasNoReference).toEqual({ + id: '1', + type: 'reference', + }); + }); + + it('accepts the query parameter has_no_reference as an array', async () => { + const references = querystring.escape( + JSON.stringify([ + { + id: '1', + type: 'reference', + }, + { + id: '2', + type: 'reference', + }, + ]) + ); + await supertest(httpSetup.server.listener) + .get(`/api/saved_objects/_find?type=foo&has_no_reference=${references}`) + .expect(200); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + + const options = savedObjectsClient.find.mock.calls[0][0]; + expect(options.hasNoReference).toEqual([ + { + id: '1', + type: 'reference', + }, + { + id: '2', + type: 'reference', + }, + ]); + }); + + it('accepts the query parameter has_no_reference_operator', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/_find?type=foo&has_no_reference_operator=AND') + .expect(200); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + + const options = savedObjectsClient.find.mock.calls[0][0]; + expect(options).toEqual( + expect.objectContaining({ + hasNoReferenceOperator: 'AND', + }) + ); + }); + it('accepts the query parameter search_fields', async () => { await supertest(httpSetup.server.listener) .get('/api/saved_objects/_find?type=foo&search_fields=title') diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index b4224e154def..6f82ec078f7a 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -11,7 +11,7 @@ export const storybookAliases = { apm: 'x-pack/plugins/apm/.storybook', canvas: 'x-pack/plugins/canvas/storybook', ci_composite: '.ci/.storybook', - cloud: 'x-pack/plugins/cloud/.storybook', + cloud_chat: 'x-pack/plugins/cloud_integrations/cloud_chat/.storybook', coloring: 'packages/kbn-coloring/.storybook', chart_icons: 'packages/kbn-chart-icons/.storybook', content_management: 'packages/content-management/.storybook', diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 0f51f5da6235..85720480cf9a 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -20,6 +20,7 @@ import { } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { map$ } from '@kbn/std'; +import { RouteConfigOptions } from '@kbn/core-http-server'; import { StreamingResponseHandler, BatchRequestData, @@ -54,7 +55,8 @@ export interface BfetchServerSetup { context: RequestHandlerContext ) => StreamingResponseHandler, method?: 'GET' | 'POST' | 'PUT' | 'DELETE', - pluginRouter?: ReturnType + pluginRouter?: ReturnType, + options?: RouteConfigOptions<'get' | 'post' | 'put' | 'delete'> ) => void; } @@ -117,14 +119,16 @@ export class BfetchServerPlugin router: ReturnType; logger: Logger; }): BfetchServerSetup['addStreamingResponseRoute'] => - (path, handler, method = 'POST', pluginRouter) => { + (path, handler, method = 'POST', pluginRouter, options) => { const httpRouter = pluginRouter || router; + const routeDefinition = { path: `/${removeLeadingSlash(path)}`, validate: { body: schema.any(), query: schema.object({ compress: schema.boolean({ defaultValue: false }) }), }, + options, }; const routeHandler: RequestHandler = async ( context: RequestHandlerContext, diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index 095dde1c2950..67b0e2c0d957 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -77,9 +77,9 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => { const [polling, setPolling] = useState(props.settings.polling); const [pollInterval, setPollInterval] = useState(props.settings.pollInterval); const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes); - const [isHistoryDisabled, setIsHistoryDisabled] = useState(props.settings.isHistoryDisabled); - const [isKeyboardShortcutsDisabled, setIsKeyboardShortcutsDisabled] = useState( - props.settings.isKeyboardShortcutsDisabled + const [isHistoryEnabled, setIsHistoryEnabled] = useState(props.settings.isHistoryEnabled); + const [isKeyboardShortcutsEnabled, setIsKeyboardShortcutsEnabled] = useState( + props.settings.isKeyboardShortcutsEnabled ); const autoCompleteCheckboxes = [ @@ -140,8 +140,8 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => { polling, pollInterval, tripleQuotes, - isHistoryDisabled, - isKeyboardShortcutsDisabled, + isHistoryEnabled, + isKeyboardShortcutsEnabled, }); } @@ -153,17 +153,17 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => { }, []); const toggleKeyboardShortcuts = useCallback( - (isDisabled: boolean) => { + (isEnabled: boolean) => { if (props.editorInstance) { unregisterCommands(props.editorInstance); - setIsKeyboardShortcutsDisabled(isDisabled); + setIsKeyboardShortcutsEnabled(isEnabled); } }, [props.editorInstance] ); const toggleSavingToHistory = useCallback( - (isDisabled: boolean) => setIsHistoryDisabled(isDisabled), + (isEnabled: boolean) => setIsHistoryEnabled(isEnabled), [] ); @@ -289,11 +289,11 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => { } > } onChange={(e) => toggleSavingToHistory(e.target.checked)} @@ -309,11 +309,11 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => { } > } onChange={(e) => toggleKeyboardShortcuts(e.target.checked)} diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 74a052646e19..ed8c87b5df14 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -259,8 +259,8 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { }, [settings]); useEffect(() => { - const { isKeyboardShortcutsDisabled } = settings; - if (!isKeyboardShortcutsDisabled) { + const { isKeyboardShortcutsEnabled } = settings; + if (isKeyboardShortcutsEnabled) { registerCommands({ senseEditor: editorInstanceRef.current!, sendCurrentRequest, diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx index 0c7e4c46d95a..e895ddc135db 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx +++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx @@ -106,7 +106,9 @@ describe('useSendCurrentRequest', () => { (sendRequest as jest.Mock).mockReturnValue( [{ request: {} }, { request: {} }] /* two responses to save history */ ); - (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); + (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({ + isHistoryEnabled: true, + }); (mockContextValue.services.history.addToHistory as jest.Mock).mockImplementation(() => { // Mock throwing throw new Error('cannot save!'); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts index 87f72571a63e..28d875c246ca 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts @@ -52,9 +52,9 @@ export const useSendCurrentRequest = () => { const results = await sendRequest({ http, requests }); let saveToHistoryError: undefined | Error; - const { isHistoryDisabled } = settings.toJSON(); + const { isHistoryEnabled } = settings.toJSON(); - if (!isHistoryDisabled) { + if (isHistoryEnabled) { results.forEach(({ request: { path, method, data } }) => { try { history.addToHistory(path, method, data); @@ -84,7 +84,7 @@ export const useSendCurrentRequest = () => { notifications.toasts.remove(toast); }, onDisableSavingToHistory: () => { - settings.setIsHistoryDisabled(true); + settings.setIsHistoryEnabled(false); notifications.toasts.remove(toast); }, }), diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts index aa2280f06064..e4731dd3f3a3 100644 --- a/src/plugins/console/public/services/settings.ts +++ b/src/plugins/console/public/services/settings.ts @@ -15,8 +15,8 @@ export const DEFAULT_SETTINGS = Object.freeze({ tripleQuotes: true, wrapMode: true, autocomplete: Object.freeze({ fields: true, indices: true, templates: true, dataStreams: true }), - isHistoryDisabled: false, - isKeyboardShortcutsDisabled: false, + isHistoryEnabled: true, + isKeyboardShortcutsEnabled: true, }); export interface DevToolsSettings { @@ -31,8 +31,8 @@ export interface DevToolsSettings { polling: boolean; pollInterval: number; tripleQuotes: boolean; - isHistoryDisabled: boolean; - isKeyboardShortcutsDisabled: boolean; + isHistoryEnabled: boolean; + isKeyboardShortcutsEnabled: boolean; } enum SettingKeys { @@ -42,12 +42,32 @@ enum SettingKeys { AUTOCOMPLETE_SETTINGS = 'autocomplete_settings', CONSOLE_POLLING = 'console_polling', POLL_INTERVAL = 'poll_interval', - IS_HISTORY_DISABLED = 'is_history_disabled', - IS_KEYBOARD_SHORTCUTS_DISABLED = 'is_keyboard_shortcuts_disabled', + IS_HISTORY_ENABLED = 'is_history_enabled', + IS_KEYBOARD_SHORTCUTS_ENABLED = 'is_keyboard_shortcuts_enabled', } export class Settings { - constructor(private readonly storage: Storage) {} + constructor(private readonly storage: Storage) { + // Migration from old settings to new ones + this.addMigrationRule('is_history_disabled', SettingKeys.IS_HISTORY_ENABLED, (value: any) => { + return !value; + }); + this.addMigrationRule( + 'is_keyboard_shortcuts_disabled', + SettingKeys.IS_KEYBOARD_SHORTCUTS_ENABLED, + (value: any) => { + return !value; + } + ); + } + + private addMigrationRule(previousKey: string, newKey: string, migration: (value: any) => any) { + const value = this.storage.get(previousKey); + if (value !== undefined) { + this.storage.set(newKey, migration(value)); + this.storage.delete(previousKey); + } + } getFontSize() { return this.storage.get(SettingKeys.FONT_SIZE, DEFAULT_SETTINGS.fontSize); @@ -94,13 +114,13 @@ export class Settings { return true; } - setIsHistoryDisabled(isDisabled: boolean) { - this.storage.set(SettingKeys.IS_HISTORY_DISABLED, isDisabled); + setIsHistoryEnabled(isEnabled: boolean) { + this.storage.set(SettingKeys.IS_HISTORY_ENABLED, isEnabled); return true; } - getIsHistoryDisabled() { - return this.storage.get(SettingKeys.IS_HISTORY_DISABLED, DEFAULT_SETTINGS.isHistoryDisabled); + getIsHistoryEnabled() { + return this.storage.get(SettingKeys.IS_HISTORY_ENABLED, DEFAULT_SETTINGS.isHistoryEnabled); } setPollInterval(interval: number) { @@ -111,15 +131,15 @@ export class Settings { return this.storage.get(SettingKeys.POLL_INTERVAL, DEFAULT_SETTINGS.pollInterval); } - setIsKeyboardShortcutsDisabled(disable: boolean) { - this.storage.set(SettingKeys.IS_KEYBOARD_SHORTCUTS_DISABLED, disable); + setIsKeyboardShortcutsEnabled(isEnabled: boolean) { + this.storage.set(SettingKeys.IS_KEYBOARD_SHORTCUTS_ENABLED, isEnabled); return true; } getIsKeyboardShortcutsDisabled() { return this.storage.get( - SettingKeys.IS_KEYBOARD_SHORTCUTS_DISABLED, - DEFAULT_SETTINGS.isKeyboardShortcutsDisabled + SettingKeys.IS_KEYBOARD_SHORTCUTS_ENABLED, + DEFAULT_SETTINGS.isKeyboardShortcutsEnabled ); } @@ -131,8 +151,8 @@ export class Settings { fontSize: parseFloat(this.getFontSize()), polling: Boolean(this.getPolling()), pollInterval: this.getPollInterval(), - isHistoryDisabled: Boolean(this.getIsHistoryDisabled()), - isKeyboardShortcutsDisabled: Boolean(this.getIsKeyboardShortcutsDisabled()), + isHistoryEnabled: Boolean(this.getIsHistoryEnabled()), + isKeyboardShortcutsEnabled: Boolean(this.getIsKeyboardShortcutsDisabled()), }; } @@ -143,8 +163,8 @@ export class Settings { autocomplete, polling, pollInterval, - isHistoryDisabled, - isKeyboardShortcutsDisabled, + isHistoryEnabled, + isKeyboardShortcutsEnabled, }: DevToolsSettings) { this.setFontSize(fontSize); this.setWrapMode(wrapMode); @@ -152,8 +172,8 @@ export class Settings { this.setAutocomplete(autocomplete); this.setPolling(polling); this.setPollInterval(pollInterval); - this.setIsHistoryDisabled(isHistoryDisabled); - this.setIsKeyboardShortcutsDisabled(isKeyboardShortcutsDisabled); + this.setIsHistoryEnabled(isHistoryEnabled); + this.setIsKeyboardShortcutsEnabled(isKeyboardShortcutsEnabled); } } diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index 02b33e814e2a..72b4d6cb8fd0 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -8,6 +8,6 @@ "server": true, "ui": true, "requiredPlugins": ["dataViews", "share", "urlForwarding"], - "optionalPlugins": ["usageCollection", "customIntegrations"], + "optionalPlugins": ["usageCollection", "customIntegrations", "cloud"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 12243944ef0f..a6c6012a28ed 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -11,6 +11,7 @@ import { HomePublicPlugin } from './plugin'; import { coreMock } from '@kbn/core/public/mocks'; import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks'; import { SharePluginSetup } from '@kbn/share-plugin/public'; +import { cloudMock } from '@kbn/cloud-plugin/public/mocks'; const mockInitializerContext = coreMock.createPluginInitializerContext(); const mockShare = {} as SharePluginSetup; @@ -24,14 +25,11 @@ describe('HomePublicPlugin', () => { }); describe('setup', () => { - test('registers tutorial directory to feature catalogue', async () => { - const setup = await new HomePublicPlugin(mockInitializerContext).setup( - coreMock.createSetup() as any, - { - share: mockShare, - urlForwarding: urlForwardingPluginMock.createSetupContract(), - } - ); + test('registers tutorial directory to feature catalogue', () => { + const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), { + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); expect(setup).toHaveProperty('featureCatalogue'); expect(setup.featureCatalogue.register).toHaveBeenCalledTimes(1); expect(setup.featureCatalogue.register).toHaveBeenCalledWith( @@ -44,53 +42,73 @@ describe('HomePublicPlugin', () => { ); }); - test('wires up and returns registry', async () => { - const setup = await new HomePublicPlugin(mockInitializerContext).setup( - coreMock.createSetup() as any, - { - share: mockShare, - urlForwarding: urlForwardingPluginMock.createSetupContract(), - } - ); + test('wires up and returns registry', () => { + const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), { + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); expect(setup).toHaveProperty('featureCatalogue'); expect(setup.featureCatalogue).toHaveProperty('register'); }); - test('wires up and returns environment service', async () => { - const setup = await new HomePublicPlugin(mockInitializerContext).setup( - coreMock.createSetup() as any, - { - share: {} as SharePluginSetup, - urlForwarding: urlForwardingPluginMock.createSetupContract(), - } - ); + test('wires up and returns environment service', () => { + const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), { + share: {} as SharePluginSetup, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); expect(setup).toHaveProperty('environment'); expect(setup.environment).toHaveProperty('update'); }); - test('wires up and returns tutorial service', async () => { - const setup = await new HomePublicPlugin(mockInitializerContext).setup( - coreMock.createSetup() as any, - { - share: mockShare, - urlForwarding: urlForwardingPluginMock.createSetupContract(), - } - ); + test('wires up and returns tutorial service', () => { + const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), { + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); expect(setup).toHaveProperty('tutorials'); expect(setup.tutorials).toHaveProperty('setVariable'); }); - test('wires up and returns welcome service', async () => { - const setup = await new HomePublicPlugin(mockInitializerContext).setup( - coreMock.createSetup() as any, - { - share: mockShare, - urlForwarding: urlForwardingPluginMock.createSetupContract(), - } - ); + test('wires up and returns welcome service', () => { + const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), { + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); expect(setup).toHaveProperty('welcomeScreen'); expect(setup.welcomeScreen).toHaveProperty('registerOnRendered'); expect(setup.welcomeScreen).toHaveProperty('registerTelemetryNoticeRenderer'); }); + + test('sets the cloud environment variable when the cloud plugin is present but isCloudEnabled: false', () => { + const cloud = { ...cloudMock.createSetup(), isCloudEnabled: false }; + const plugin = new HomePublicPlugin(mockInitializerContext); + const setup = plugin.setup(coreMock.createSetup(), { + cloud, + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); + expect(setup.environment.update).toHaveBeenCalledTimes(1); + expect(setup.environment.update).toHaveBeenCalledWith({ cloud: false }); + expect(setup.tutorials.setVariable).toHaveBeenCalledTimes(0); + }); + + test('when cloud is enabled, it sets the cloud environment and the tutorials variable "cloud"', () => { + const cloud = { ...cloudMock.createSetup(), isCloudEnabled: true }; + const plugin = new HomePublicPlugin(mockInitializerContext); + const setup = plugin.setup(coreMock.createSetup(), { + cloud, + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); + expect(setup.environment.update).toHaveBeenCalledTimes(1); + expect(setup.environment.update).toHaveBeenCalledWith({ cloud: true }); + expect(setup.tutorials.setVariable).toHaveBeenCalledTimes(1); + expect(setup.tutorials.setVariable).toHaveBeenCalledWith('cloud', { + id: 'mock-cloud-id', + baseUrl: 'base-url', + deploymentUrl: 'deployment-url', + profileUrl: 'profile-url', + }); + }); }); }); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 642a8d575e07..e27ddf107a5e 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -20,6 +20,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; import { AppNavLinkStatus } from '@kbn/core/public'; import { SharePluginSetup } from '@kbn/share-plugin/public'; +import type { CloudSetup } from '@kbn/cloud-plugin/public'; import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; import { setServices } from './application/kibana_services'; import { ConfigSchema } from '../config'; @@ -42,6 +43,7 @@ export interface HomePluginStartDependencies { } export interface HomePluginSetupDependencies { + cloud?: CloudSetup; share: SharePluginSetup; usageCollection?: UsageCollectionSetup; urlForwarding: UrlForwardingSetup; @@ -66,7 +68,7 @@ export class HomePublicPlugin public setup( core: CoreSetup, - { share, urlForwarding, usageCollection }: HomePluginSetupDependencies + { cloud, share, urlForwarding, usageCollection }: HomePluginSetupDependencies ): HomePublicPluginSetup { core.application.register({ id: PLUGIN_ID, @@ -127,10 +129,25 @@ export class HomePublicPlugin order: 500, }); + const environment = { ...this.environmentService.setup() }; + const tutorials = { ...this.tutorialService.setup() }; + if (cloud) { + environment.update({ cloud: cloud.isCloudEnabled }); + if (cloud.isCloudEnabled) { + tutorials.setVariable('cloud', { + id: cloud.cloudId, + baseUrl: cloud.baseUrl, + // Cloud's API already provides the full URLs + profileUrl: cloud.profileUrl?.replace(cloud.baseUrl ?? '', ''), + deploymentUrl: cloud.deploymentUrl?.replace(cloud.baseUrl ?? '', ''), + }); + } + } + return { featureCatalogue, - environment: { ...this.environmentService.setup() }, - tutorials: { ...this.tutorialService.setup() }, + environment, + tutorials, addData: { ...this.addDataService.setup() }, welcomeScreen: { ...this.welcomeService.setup() }, }; diff --git a/src/plugins/home/public/services/environment/environment.mock.ts b/src/plugins/home/public/services/environment/environment.mock.ts index 713a59ceac7b..f2d4747d44d6 100644 --- a/src/plugins/home/public/services/environment/environment.mock.ts +++ b/src/plugins/home/public/services/environment/environment.mock.ts @@ -18,14 +18,13 @@ const createSetupMock = (): jest.Mocked => { const createMock = (): jest.Mocked> => { const service = { - setup: jest.fn(), + setup: jest.fn(createSetupMock), getEnvironment: jest.fn(() => ({ cloud: false, apmUi: false, ml: false, })), }; - service.setup.mockImplementation(createSetupMock); return service; }; diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index 8e617896e3f9..af121720eee0 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -15,6 +15,7 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../url_forwarding/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" } + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../../../x-pack/plugins/cloud/tsconfig.json" } ] } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts index 6c94971397d3..46e9d9e1fae2 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts @@ -24,6 +24,33 @@ jest.mock('uuid', () => ({ v4: () => 'test-id', })); +const mockedIndices = [ + { + id: 'test', + title: 'test', + timeFieldName: 'test_field', + getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }), + }, +] as unknown as DataView[]; + +const indexPatternsService = { + getDefault: jest.fn(() => + Promise.resolve({ + id: 'default', + title: 'index', + getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }), + }) + ), + get: jest.fn((id) => Promise.resolve({ ...mockedIndices[0], id })), + find: jest.fn((search: string, size: number) => { + if (size !== 1) { + // shouldn't request more than one data view since there is a significant performance penalty + throw new Error('trying to fetch too many data views'); + } + return Promise.resolve(mockedIndices || []); + }), +} as unknown as DataViewsPublicPluginStart; + describe('getLayers', () => { const dataSourceLayers: Record = [ { @@ -331,10 +358,16 @@ describe('getLayers', () => { series: [createSeries({ metrics: staticValueMetric })], }); - test.each<[string, [Record, Panel], Array>]>([ + test.each< + [ + string, + [Record, Panel, DataViewsPublicPluginStart, boolean], + Array> + ] + >([ [ 'data layer if columns do not include static column', - [dataSourceLayers, panel], + [dataSourceLayers, panel, indexPatternsService, false], [ { layerType: 'data', @@ -353,9 +386,30 @@ describe('getLayers', () => { }, ], ], + [ + 'data layer with "left" axisMode if isSingleAxis is provided', + [dataSourceLayers, panel, indexPatternsService, true], + [ + { + layerType: 'data', + accessors: ['column-id-1'], + xAccessor: 'column-id-2', + splitAccessor: 'column-id-3', + seriesType: 'area', + layerId: 'test-layer-1', + yConfig: [ + { + forAccessor: 'column-id-1', + axisMode: 'left', + color: '#68BC00', + }, + ], + }, + ], + ], [ 'reference line layer if columns include static column', - [dataSourceLayersWithStatic, panelWithStaticValue], + [dataSourceLayersWithStatic, panelWithStaticValue, indexPatternsService, false], [ { layerType: 'referenceLine', @@ -364,9 +418,10 @@ describe('getLayers', () => { yConfig: [ { forAccessor: 'column-id-1', - axisMode: 'right', + axisMode: 'left', color: '#68BC00', fill: 'below', + lineWidth: 1, }, ], }, @@ -374,7 +429,7 @@ describe('getLayers', () => { ], [ 'correct colors if columns include percentile columns', - [dataSourceLayersWithPercentile, panelWithPercentileMetric], + [dataSourceLayersWithPercentile, panelWithPercentileMetric, indexPatternsService, false], [ { yConfig: [ @@ -394,7 +449,12 @@ describe('getLayers', () => { ], [ 'correct colors if columns include percentile rank columns', - [dataSourceLayersWithPercentileRank, panelWithPercentileRankMetric], + [ + dataSourceLayersWithPercentileRank, + panelWithPercentileRankMetric, + indexPatternsService, + false, + ], [ { yConfig: [ @@ -414,7 +474,7 @@ describe('getLayers', () => { ], [ 'annotation layer gets correct params and converts color, extraFields and icons', - [dataSourceLayersWithStatic, panelWithSingleAnnotation], + [dataSourceLayersWithStatic, panelWithSingleAnnotation, indexPatternsService, false], [ { layerType: 'referenceLine', @@ -423,9 +483,10 @@ describe('getLayers', () => { yConfig: [ { forAccessor: 'column-id-1', - axisMode: 'right', + axisMode: 'left', color: '#68BC00', fill: 'below', + lineWidth: 1, }, ], }, @@ -459,7 +520,12 @@ describe('getLayers', () => { ], [ 'annotation layer should gets correct default params', - [dataSourceLayersWithStatic, panelWithSingleAnnotationWithoutQueryStringAndTimefield], + [ + dataSourceLayersWithStatic, + panelWithSingleAnnotationWithoutQueryStringAndTimefield, + indexPatternsService, + false, + ], [ { layerType: 'referenceLine', @@ -468,9 +534,10 @@ describe('getLayers', () => { yConfig: [ { forAccessor: 'column-id-1', - axisMode: 'right', + axisMode: 'left', color: '#68BC00', fill: 'below', + lineWidth: 1, }, ], }, @@ -504,7 +571,7 @@ describe('getLayers', () => { ], [ 'multiple annotations with different data views create separate layers', - [dataSourceLayersWithStatic, panelWithMultiAnnotations], + [dataSourceLayersWithStatic, panelWithMultiAnnotations, indexPatternsService, false], [ { layerType: 'referenceLine', @@ -513,9 +580,10 @@ describe('getLayers', () => { yConfig: [ { forAccessor: 'column-id-1', - axisMode: 'right', + axisMode: 'left', color: '#68BC00', fill: 'below', + lineWidth: 1, }, ], }, @@ -598,7 +666,12 @@ describe('getLayers', () => { ], [ 'annotation layer gets correct dataView when none is defined', - [dataSourceLayersWithStatic, panelWithSingleAnnotationDefaultDataView], + [ + dataSourceLayersWithStatic, + panelWithSingleAnnotationDefaultDataView, + indexPatternsService, + false, + ], [ { layerType: 'referenceLine', @@ -607,9 +680,10 @@ describe('getLayers', () => { yConfig: [ { forAccessor: 'column-id-1', - axisMode: 'right', + axisMode: 'left', color: '#68BC00', fill: 'below', + lineWidth: 1, }, ], }, @@ -642,34 +716,7 @@ describe('getLayers', () => { ], ], ])('should return %s', async (_, input, expected) => { - const layers = await getLayers(...input, indexPatternsService as DataViewsPublicPluginStart); + const layers = await getLayers(...input); expect(layers).toEqual(expected.map(expect.objectContaining)); }); }); - -const mockedIndices = [ - { - id: 'test', - title: 'test', - timeFieldName: 'test_field', - getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }), - }, -] as unknown as DataView[]; - -const indexPatternsService = { - getDefault: jest.fn(() => - Promise.resolve({ - id: 'default', - title: 'index', - getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }), - }) - ), - get: jest.fn((id) => Promise.resolve({ ...mockedIndices[0], id })), - find: jest.fn((search: string, size: number) => { - if (size !== 1) { - // shouldn't request more than one data view since there is a significant performance penalty - throw new Error('trying to fetch too many data views'); - } - return Promise.resolve(mockedIndices || []); - }), -} as unknown as DataViewsPublicPluginStart; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts index ec0e24e2db87..8784c2952807 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts @@ -24,7 +24,7 @@ import { getDefaultQueryLanguage } from '../../../../application/components/lib/ import { fetchIndexPattern } from '../../../../../common/index_patterns_utils'; import { ICON_TYPES_MAP } from '../../../../application/visualizations/constants'; import { SUPPORTED_METRICS } from '../../metrics'; -import type { Annotation, Metric, Panel } from '../../../../../common/types'; +import type { Annotation, Metric, Panel, Series } from '../../../../../common/types'; import { getSeriesAgg } from '../../series'; import { isPercentileRanksColumnWithMeta, @@ -44,6 +44,10 @@ function getPalette(palette: PaletteOutput): PaletteOutput { : palette; } +function getAxisMode(series: Series, model: Panel): YAxisMode { + return (series.separate_axis ? series.axis_position : model.axis_position) as YAxisMode; +} + function getColor( metricColumn: Column, metric: Metric, @@ -69,7 +73,8 @@ function nonNullable(value: T): value is NonNullable { export const getLayers = async ( dataSourceLayers: Record, model: Panel, - dataViews: DataViewsPublicPluginStart + dataViews: DataViewsPublicPluginStart, + isSingleAxis: boolean = false ): Promise => { const nonAnnotationsLayers: XYLayerConfig[] = Object.keys(dataSourceLayers).map((key) => { const series = model.series[parseInt(key, 10)]; @@ -84,13 +89,13 @@ export const getLayers = async ( const metricColumns = dataSourceLayer.columns.filter( (l) => !l.isBucketed && l.columnId !== referenceColumnId ); - const isReferenceLine = metrics.length === 1 && metrics[0].type === 'static'; + const isReferenceLine = + metricColumns.length === 1 && metricColumns[0].operationType === 'static_value'; const splitAccessor = dataSourceLayer.columns.find( (column) => column.isBucketed && column.isSplit )?.columnId; const chartType = getChartType(series, model.type); const commonProps = { - seriesType: chartType, layerId: dataSourceLayer.layerId, accessors: metricColumns.map((metricColumn) => { return metricColumn.columnId; @@ -102,19 +107,19 @@ export const getLayers = async ( return { forAccessor: metricColumn.columnId, color: getColor(metricColumn, metric!, series.color, splitAccessor), - axisMode: (series.separate_axis - ? series.axis_position - : model.axis_position) as YAxisMode, + axisMode: isReferenceLine // reference line should be assigned to axis with real data + ? model.series.some((s) => s.id !== series.id && getAxisMode(s, model) === 'right') + ? 'right' + : 'left' + : isSingleAxis + ? 'left' + : getAxisMode(series, model), ...(isReferenceLine && { - fill: chartType === 'area' ? FillTypes.BELOW : FillTypes.NONE, + fill: chartType.includes('area') ? FillTypes.BELOW : FillTypes.NONE, + lineWidth: series.line_width, }), }; }), - xAccessor: dataSourceLayer.columns.find((column) => column.isBucketed && !column.isSplit) - ?.columnId, - splitAccessor, - collapseFn: seriesAgg, - palette: getPalette(series.palette as PaletteOutput), }; if (isReferenceLine) { return { @@ -123,8 +128,14 @@ export const getLayers = async ( }; } else { return { + seriesType: chartType, layerType: 'data', ...commonProps, + xAccessor: dataSourceLayer.columns.find((column) => column.isBucketed && !column.isSplit) + ?.columnId, + splitAccessor, + collapseFn: seriesAgg, + palette: getPalette(series.palette as PaletteOutput), }; } }); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts index 50aa1a6c6f7f..c81db38e0538 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts @@ -112,6 +112,19 @@ describe('convertToLens', () => { expect(mockGetBucketsColumns).toBeCalledTimes(1); }); + test('should return null for static value with buckets', async () => { + mockGetBucketsColumns.mockReturnValue([{}]); + mockGetMetricsColumns.mockReturnValue([ + { + operationType: 'static_value', + }, + ]); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockGetMetricsColumns).toBeCalledTimes(1); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + }); + test('should return state for valid model', async () => { const result = await convertToLens(model); expect(result).toBeDefined(); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts index 8cbbbf0f9e73..ef678fcc2dab 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts @@ -98,11 +98,21 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model: Panel return null; } + const isReferenceLine = + metricsColumns.length === 1 && metricsColumns[0].operationType === 'static_value'; + + // only static value without split is supported + if (isReferenceLine && bucketsColumns.length) { + return null; + } + const layerId = uuid(); extendedLayers[layerIdx] = { indexPatternId, layerId, - columns: [...metricsColumns, dateHistogramColumn, ...bucketsColumns], + columns: isReferenceLine + ? [...metricsColumns] + : [...metricsColumns, dateHistogramColumn, ...bucketsColumns], columnOrder: [], }; } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts index 020aaec28f57..130646f72f12 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts @@ -86,7 +86,7 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeR }; } - const configLayers = await getLayers(extendedLayers, model, dataViews); + const configLayers = await getLayers(extendedLayers, model, dataViews, true); if (configLayers === null) { return null; } diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts index bc04d60c3fb5..7b21a5637d16 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); const queryBar = getService('queryBar'); - describe('Loaded Dashboard', () => { + // Failing: See https://github.com/elastic/kibana/issues/142548 + describe.skip('Loaded Dashboard', () => { let fromTimestamp: string | undefined; const getEvents = async (count: number, options?: GetEventsOptions) => diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index 4afcc4f162a6..5c11b6f74d7a 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -338,6 +338,131 @@ export default function ({ getService }: FtrProviderContext) { }); }); + describe('`has_no_reference` and `has_no_reference_operator` parameters', () => { + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json', + { space: SPACE_ID } + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json', + { space: SPACE_ID } + ); + }); + + it('search for objects not containing a reference', async () => { + await supertest + .get(`/s/${SPACE_ID}/api/saved_objects/_find`) + .query({ + type: 'visualization', + has_no_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }), + }) + .expect(200) + .then((resp) => { + const objects = resp.body.saved_objects; + const ids = objects.map((obj: SavedObject) => obj.id); + expect(ids).to.contain('only-ref-2'); + expect(ids).to.contain('only-ref-3'); + expect(ids).not.to.contain('only-ref-1'); + expect(ids).not.to.contain('ref-1-and-ref-2'); + }); + }); + + it('search for multiple references with OR operator', async () => { + await supertest + .get(`/s/${SPACE_ID}/api/saved_objects/_find`) + .query({ + type: 'visualization', + has_no_reference: JSON.stringify([ + { type: 'ref-type', id: 'ref-1' }, + { type: 'ref-type', id: 'ref-2' }, + ]), + has_no_reference_operator: 'OR', + }) + .expect(200) + .then((resp) => { + const objects = resp.body.saved_objects; + const ids = objects.map((obj: SavedObject) => obj.id); + + expect(ids).to.contain('only-ref-3'); + expect(ids).not.to.contain('only-ref-1'); + expect(ids).not.to.contain('only-ref-2'); + expect(ids).not.to.contain('ref-1-and-ref-2'); + }); + }); + + it('search for multiple references with AND operator', async () => { + await supertest + .get(`/s/${SPACE_ID}/api/saved_objects/_find`) + .query({ + type: 'visualization', + has_no_reference: JSON.stringify([ + { type: 'ref-type', id: 'ref-1' }, + { type: 'ref-type', id: 'ref-2' }, + ]), + has_no_reference_operator: 'AND', + }) + .expect(200) + .then((resp) => { + const objects = resp.body.saved_objects; + const ids = objects.map((obj: SavedObject) => obj.id); + expect(ids).to.contain('only-ref-1'); + expect(ids).to.contain('only-ref-2'); + expect(ids).to.contain('only-ref-3'); + expect(ids).not.to.contain('ref-1-and-ref-2'); + }); + }); + }); + + describe('with both `has_reference` and `has_no_reference` parameters', () => { + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json', + { space: SPACE_ID } + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json', + { space: SPACE_ID } + ); + }); + + it('search for objects containing a reference and excluding another reference', async () => { + await supertest + .get(`/s/${SPACE_ID}/api/saved_objects/_find`) + .query({ + type: 'visualization', + has_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }), + has_no_reference: JSON.stringify({ type: 'ref-type', id: 'ref-2' }), + }) + .expect(200) + .then((resp) => { + const objects = resp.body.saved_objects; + const ids = objects.map((obj: SavedObject) => obj.id); + expect(ids).to.eql(['only-ref-1']); + }); + }); + + it('search for objects with same reference passed to `has_reference` and `has_no_reference`', async () => { + await supertest + .get(`/s/${SPACE_ID}/api/saved_objects/_find`) + .query({ + type: 'visualization', + has_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }), + has_no_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }), + }) + .expect(200) + .then((resp) => { + const objects = resp.body.saved_objects; + const ids = objects.map((obj: SavedObject) => obj.id); + expect(ids).to.eql([]); + }); + }); + }); + describe('searching for special characters', () => { before(async () => { await kibanaServer.importExport.load( diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index b2dbc762ab65..750da63e27d1 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -60,6 +60,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--corePluginDeprecations.noLongerUsed=still_using', // for testing set buffer duration to 0 to immediately flush counters into saved objects. '--usageCollection.usageCounters.bufferDuration=0', + // explicitly enable the cloud integration plugins to validate the rendered config keys + '--xpack.cloud_integrations.chat.enabled=true', + '--xpack.cloud_integrations.chat.chatURL=a_string', + '--xpack.cloud_integrations.experiments.enabled=true', + '--xpack.cloud_integrations.experiments.launch_darkly.sdk_key=a_string', + '--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string', + '--xpack.cloud_integrations.full_story.enabled=true', + '--xpack.cloud_integrations.full_story.org_id=a_string', ...plugins.map( (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index cbc98ec7bb07..4633a374ee9d 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -171,14 +171,17 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cases.markdownPlugins.lens (boolean)', 'xpack.ccr.ui.enabled (boolean)', 'xpack.cloud.base_url (string)', - 'xpack.cloud.chat.chatURL (string)', - 'xpack.cloud.chat.enabled (boolean)', 'xpack.cloud.cname (string)', 'xpack.cloud.deployment_url (string)', - 'xpack.cloud.full_story.enabled (boolean)', - 'xpack.cloud.full_story.org_id (any)', + 'xpack.cloud_integrations.chat.chatURL (string)', + // No PII. This is an escape patch to override LaunchDarkly's flag resolution mechanism for testing or quick fix. + 'xpack.cloud_integrations.experiments.flag_overrides (record)', + // Commented because it's inside a schema conditional, and the test is not able to resolve it. But it's shared. + // Added here for documentation purposes. + // 'xpack.cloud_integrations.experiments.launch_darkly.client_id (string)', + 'xpack.cloud_integrations.full_story.org_id (any)', // No PII. Just the list of event types we want to forward to FullStory. - 'xpack.cloud.full_story.eventTypesAllowlist (array)', + 'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)', 'xpack.cloud.id (string)', 'xpack.cloud.organization_url (string)', 'xpack.cloud.profile_url (string)', diff --git a/tsconfig.base.json b/tsconfig.base.json index b62beb665044..3054a36f2bb8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -313,8 +313,14 @@ "@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"], "@kbn/cases-plugin": ["x-pack/plugins/cases"], "@kbn/cases-plugin/*": ["x-pack/plugins/cases/*"], + "@kbn/cloud-chat-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat"], + "@kbn/cloud-chat-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat/*"], "@kbn/cloud-experiments-plugin": ["x-pack/plugins/cloud_integrations/cloud_experiments"], "@kbn/cloud-experiments-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_experiments/*"], + "@kbn/cloud-full-story-plugin": ["x-pack/plugins/cloud_integrations/cloud_full_story"], + "@kbn/cloud-full-story-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_full_story/*"], + "@kbn/cloud-links-plugin": ["x-pack/plugins/cloud_integrations/cloud_links"], + "@kbn/cloud-links-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_links/*"], "@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"], "@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"], "@kbn/cloud-plugin": ["x-pack/plugins/cloud"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 83466ba74960..4f89798c71fa 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -10,6 +10,8 @@ "xpack.canvas": "plugins/canvas", "xpack.cases": "plugins/cases", "xpack.cloud": "plugins/cloud", + "xpack.cloudChat": "plugins/cloud_integrations/cloud_chat", + "xpack.cloudLinks": "plugins/cloud_integrations/cloud_links", "xpack.csp": "plugins/cloud_security_posture", "xpack.dashboard": "plugins/dashboard_enhanced", "xpack.discover": "plugins/discover_enhanced", diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts index e050946a489b..7c4e3a47f8b7 100644 --- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts +++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts @@ -18,6 +18,7 @@ export const API_ACTION_NAME = { ADD_CHANGE_POINTS_GROUP: 'add_change_point_group', ADD_CHANGE_POINTS_GROUP_HISTOGRAM: 'add_change_point_group_histogram', ADD_ERROR: 'add_error', + PING: 'ping', RESET: 'reset', UPDATE_LOADING_STATE: 'update_loading_state', } as const; @@ -89,6 +90,14 @@ export function addErrorAction(payload: ApiActionAddError['payload']): ApiAction }; } +interface ApiActionPing { + type: typeof API_ACTION_NAME.PING; +} + +export function pingAction(): ApiActionPing { + return { type: API_ACTION_NAME.PING }; +} + interface ApiActionReset { type: typeof API_ACTION_NAME.RESET; } @@ -121,5 +130,6 @@ export type AiopsExplainLogRateSpikesApiAction = | ApiActionAddChangePointsHistogram | ApiActionAddChangePointsGroupHistogram | ApiActionAddError + | ApiActionPing | ApiActionReset | ApiActionUpdateLoadingState; diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts index 5628b509980a..c092b34c8b2b 100644 --- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts +++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts @@ -11,6 +11,7 @@ export { addChangePointsGroupHistogramAction, addChangePointsHistogramAction, addErrorAction, + pingAction, resetAction, updateLoadingStateAction, API_ACTION_NAME, diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index 242516161591..9949ec537b77 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -172,6 +172,33 @@ export const ExplainLogRateSpikesAnalysis: FC onCancel={cancel} shouldRerunAnalysis={shouldRerunAnalysis} /> + {errors.length > 0 ? ( + <> + + + {errors.length === 1 ? ( +

{errors[0]}

+ ) : ( +
    + {errors.map((e, i) => ( +
  • {e}
  • + ))} +
+ )} +
+
+ + + ) : null} {showSpikeAnalysisTable && foundGroups && ( } /> )} - {errors.length > 0 && ( - <> - - - {errors.length === 1 ? ( -

{errors[0]}

- ) : ( -
    - {errors.map((e, i) => ( -
  • {e}
  • - ))} -
- )} -
-
- - - )} {showSpikeAnalysisTable && groupResults && foundGroups ? ( { + logInfoMessage('aborted$ subscription trigger.'); shouldStop = true; controller.abort(); }); request.events.completed$.subscribe(() => { + logInfoMessage('completed$ subscription trigger.'); shouldStop = true; controller.abort(); }); - const { end, push, responseWithHeaders } = streamFactory( - request.headers, - logger, - true - ); + const { + end: streamEnd, + push, + responseWithHeaders, + } = streamFactory(request.headers, logger, true); + + function pushPing() { + push(pingAction()); + } + + const pingInterval = setInterval(pushPing, 1000); + + function end() { + logInfoMessage('Ending analysis.'); + clearInterval(pingInterval); + streamEnd(); + } function endWithUpdatedLoadingState() { push( @@ -114,9 +138,16 @@ export const defineExplainLogRateSpikesRoute = ( end(); } + function pushError(m: string) { + logInfoMessage('Push error.'); + push(addErrorAction(m)); + } + // Async IIFE to run the analysis while not blocking returning `responseWithHeaders`. (async () => { + logInfoMessage('Reset.'); push(resetAction()); + logInfoMessage('Load field candidates.'); push( updateLoadingStateAction({ ccsWarning: false, @@ -134,7 +165,8 @@ export const defineExplainLogRateSpikesRoute = ( try { fieldCandidates = await fetchFieldCandidates(client, request.body); } catch (e) { - push(addErrorAction(e.toString())); + logger.error(`Failed to fetch field candidates, got: \n${e.toString()}`); + pushError(`Failed to fetch field candidates.`); end(); return; } @@ -168,17 +200,33 @@ export const defineExplainLogRateSpikesRoute = ( const changePoints: ChangePoint[] = []; const fieldsToSample = new Set(); const chunkSize = 10; + let chunkCount = 0; const fieldCandidatesChunks = chunk(fieldCandidates, chunkSize); + logInfoMessage('Fetch p-values.'); + for (const fieldCandidatesChunk of fieldCandidatesChunks) { + chunkCount++; + logInfoMessage(`Fetch p-values. Chunk ${chunkCount} of ${fieldCandidatesChunks.length}`); let pValues: Awaited>; try { - pValues = await fetchChangePointPValues(client, request.body, fieldCandidatesChunk); + pValues = await fetchChangePointPValues( + client, + request.body, + fieldCandidatesChunk, + logger, + pushError + ); } catch (e) { - push(addErrorAction(e.toString())); - end(); - return; + logger.error( + `Failed to fetch p-values for ${JSON.stringify( + fieldCandidatesChunk + )}, got: \n${e.toString()}` + ); + pushError(`Failed to fetch p-values for ${JSON.stringify(fieldCandidatesChunk)}.`); + // Still continue the analysis even if chunks of p-value queries fail. + continue; } if (pValues.length > 0) { @@ -210,12 +258,15 @@ export const defineExplainLogRateSpikesRoute = ( ); if (shouldStop) { + logInfoMessage('shouldStop fetching p-values.'); + end(); return; } } if (changePoints?.length === 0) { + logInfoMessage('Stopping analysis, did not find change points.'); endWithUpdatedLoadingState(); return; } @@ -224,16 +275,27 @@ export const defineExplainLogRateSpikesRoute = ( { fieldName: request.body.timeFieldName, type: KBN_FIELD_TYPES.DATE }, ]; - const [overallTimeSeries] = (await fetchHistogramsForFields( - client, - request.body.index, - { match_all: {} }, - // fields - histogramFields, - // samplerShardSize - -1, - undefined - )) as [NumericChartData]; + logInfoMessage('Fetch overall histogram.'); + + let overallTimeSeries: NumericChartData | undefined; + try { + overallTimeSeries = ( + (await fetchHistogramsForFields( + client, + request.body.index, + { match_all: {} }, + // fields + histogramFields, + // samplerShardSize + -1, + undefined + )) as [NumericChartData] + )[0]; + } catch (e) { + logger.error(`Failed to fetch the overall histogram data, got: \n${e.toString()}`); + pushError(`Failed to fetch overall histogram data.`); + // Still continue the analysis even if loading the overall histogram fails. + } function pushHistogramDataLoadingState() { push( @@ -251,6 +313,8 @@ export const defineExplainLogRateSpikesRoute = ( } if (groupingEnabled) { + logInfoMessage('Group results.'); + push( updateLoadingStateAction({ ccsWarning: false, @@ -283,208 +347,242 @@ export const defineExplainLogRateSpikesRoute = ( (g) => g.group.length > 1 ); - const { fields, df } = await fetchFrequentItems( - client, - request.body.index, - JSON.parse(request.body.searchQuery) as estypes.QueryDslQueryContainer, - deduplicatedChangePoints, - request.body.timeFieldName, - request.body.deviationMin, - request.body.deviationMax - ); - - // The way the `frequent_items` aggregations works could return item sets that include - // field/value pairs that are not part of the original list of significant change points. - // This cleans up groups and removes those unrelated field/value pairs. - const filteredDf = df - .map((fi) => { - fi.set = Object.entries(fi.set).reduce( - (set, [field, value]) => { - if ( - changePoints.some((cp) => cp.fieldName === field && cp.fieldValue === value) - ) { - set[field] = value; + try { + const { fields, df } = await fetchFrequentItems( + client, + request.body.index, + JSON.parse(request.body.searchQuery) as estypes.QueryDslQueryContainer, + deduplicatedChangePoints, + request.body.timeFieldName, + request.body.deviationMin, + request.body.deviationMax, + logger, + pushError + ); + + if (fields.length > 0 && df.length > 0) { + // The way the `frequent_items` aggregations works could return item sets that include + // field/value pairs that are not part of the original list of significant change points. + // This cleans up groups and removes those unrelated field/value pairs. + const filteredDf = df + .map((fi) => { + fi.set = Object.entries(fi.set).reduce( + (set, [field, value]) => { + if ( + changePoints.some((cp) => cp.fieldName === field && cp.fieldValue === value) + ) { + set[field] = value; + } + return set; + }, + {} + ); + fi.size = Object.keys(fi.set).length; + return fi; + }) + .filter((fi) => fi.size > 1); + + // `frequent_items` returns lot of different small groups of field/value pairs that co-occur. + // The following steps analyse these small groups, identify overlap between these groups, + // and then summarize them in larger groups where possible. + + // Get a tree structure based on `frequent_items`. + const { root } = getSimpleHierarchicalTree(filteredDf, true, false, fields); + + // Each leave of the tree will be a summarized group of co-occuring field/value pairs. + const treeLeaves = getSimpleHierarchicalTreeLeaves(root, []); + + // To be able to display a more cleaned up results table in the UI, we identify field/value pairs + // that occur in multiple groups. This will allow us to highlight field/value pairs that are + // unique to a group in a better way. This step will also re-add duplicates we identified in the + // beginning and didn't pass on to the `frequent_items` agg. + const fieldValuePairCounts = getFieldValuePairCounts(treeLeaves); + const changePointGroups = markDuplicates(treeLeaves, fieldValuePairCounts).map( + (g) => { + const group = [...g.group]; + + for (const groupItem of g.group) { + const { duplicate } = groupItem; + const duplicates = groupedChangePoints.find((d) => + d.group.some( + (dg) => + dg.fieldName === groupItem.fieldName && + dg.fieldValue === groupItem.fieldValue + ) + ); + + if (duplicates !== undefined) { + group.push( + ...duplicates.group.map((d) => { + return { + fieldName: d.fieldName, + fieldValue: d.fieldValue, + duplicate, + }; + }) + ); + } } - return set; - }, - {} - ); - fi.size = Object.keys(fi.set).length; - return fi; - }) - .filter((fi) => fi.size > 1); - - // `frequent_items` returns lot of different small groups of field/value pairs that co-occur. - // The following steps analyse these small groups, identify overlap between these groups, - // and then summarize them in larger groups where possible. - - // Get a tree structure based on `frequent_items`. - const { root } = getSimpleHierarchicalTree(filteredDf, true, false, fields); - - // Each leave of the tree will be a summarized group of co-occuring field/value pairs. - const treeLeaves = getSimpleHierarchicalTreeLeaves(root, []); - - // To be able to display a more cleaned up results table in the UI, we identify field/value pairs - // that occur in multiple groups. This will allow us to highlight field/value pairs that are - // unique to a group in a better way. This step will also re-add duplicates we identified in the - // beginning and didn't pass on to the `frequent_items` agg. - const fieldValuePairCounts = getFieldValuePairCounts(treeLeaves); - const changePointGroups = markDuplicates(treeLeaves, fieldValuePairCounts).map((g) => { - const group = [...g.group]; - - for (const groupItem of g.group) { - const { duplicate } = groupItem; - const duplicates = groupedChangePoints.find((d) => - d.group.some( - (dg) => - dg.fieldName === groupItem.fieldName && dg.fieldValue === groupItem.fieldValue - ) - ); - - if (duplicates !== undefined) { - group.push( - ...duplicates.group.map((d) => { - return { - fieldName: d.fieldName, - fieldValue: d.fieldValue, - duplicate, - }; - }) - ); - } - } - return { - ...g, - group, - }; - }); - - // Some field/value pairs might not be part of the `frequent_items` result set, for example - // because they don't co-occur with other field/value pairs or because of the limits we set on the query. - // In this next part we identify those missing pairs and add them as individual groups. - const missingChangePoints = deduplicatedChangePoints.filter((cp) => { - return !changePointGroups.some((cpg) => { - return cpg.group.some( - (d) => d.fieldName === cp.fieldName && d.fieldValue === cp.fieldValue + return { + ...g, + group, + }; + } ); - }); - }); - changePointGroups.push( - ...missingChangePoints.map(({ fieldName, fieldValue, doc_count: docCount, pValue }) => { - const duplicates = groupedChangePoints.find((d) => - d.group.some((dg) => dg.fieldName === fieldName && dg.fieldValue === fieldValue) + // Some field/value pairs might not be part of the `frequent_items` result set, for example + // because they don't co-occur with other field/value pairs or because of the limits we set on the query. + // In this next part we identify those missing pairs and add them as individual groups. + const missingChangePoints = deduplicatedChangePoints.filter((cp) => { + return !changePointGroups.some((cpg) => { + return cpg.group.some( + (d) => d.fieldName === cp.fieldName && d.fieldValue === cp.fieldValue + ); + }); + }); + + changePointGroups.push( + ...missingChangePoints.map( + ({ fieldName, fieldValue, doc_count: docCount, pValue }) => { + const duplicates = groupedChangePoints.find((d) => + d.group.some( + (dg) => dg.fieldName === fieldName && dg.fieldValue === fieldValue + ) + ); + if (duplicates !== undefined) { + return { + id: `${stringHash( + JSON.stringify( + duplicates.group.map((d) => ({ + fieldName: d.fieldName, + fieldValue: d.fieldValue, + })) + ) + )}`, + group: duplicates.group.map((d) => ({ + fieldName: d.fieldName, + fieldValue: d.fieldValue, + duplicate: false, + })), + docCount, + pValue, + }; + } else { + return { + id: `${stringHash(JSON.stringify({ fieldName, fieldValue }))}`, + group: [ + { + fieldName, + fieldValue, + duplicate: false, + }, + ], + docCount, + pValue, + }; + } + } + ) ); - if (duplicates !== undefined) { - return { - id: `${stringHash( - JSON.stringify( - duplicates.group.map((d) => ({ - fieldName: d.fieldName, - fieldValue: d.fieldValue, - })) - ) - )}`, - group: duplicates.group.map((d) => ({ - fieldName: d.fieldName, - fieldValue: d.fieldValue, - duplicate: false, - })), - docCount, - pValue, - }; - } else { - return { - id: `${stringHash(JSON.stringify({ fieldName, fieldValue }))}`, - group: [ - { - fieldName, - fieldValue, - duplicate: false, - }, - ], - docCount, - pValue, - }; - } - }) - ); - - // Finally, we'll find out if there's at least one group with at least two items, - // only then will we return the groups to the clients and make the grouping option available. - const maxItems = Math.max(...changePointGroups.map((g) => g.group.length)); - if (maxItems > 1) { - push(addChangePointsGroupAction(changePointGroups)); - } + // Finally, we'll find out if there's at least one group with at least two items, + // only then will we return the groups to the clients and make the grouping option available. + const maxItems = Math.max(...changePointGroups.map((g) => g.group.length)); - loaded += PROGRESS_STEP_GROUPING; + if (maxItems > 1) { + push(addChangePointsGroupAction(changePointGroups)); + } - pushHistogramDataLoadingState(); + loaded += PROGRESS_STEP_GROUPING; - if (changePointGroups) { - await asyncForEach(changePointGroups, async (cpg, index) => { - const histogramQuery = { - bool: { - filter: cpg.group.map((d) => ({ - term: { [d.fieldName]: d.fieldValue }, - })), - }, - }; + pushHistogramDataLoadingState(); - const [cpgTimeSeries] = (await fetchHistogramsForFields( - client, - request.body.index, - histogramQuery, - // fields - [ - { - fieldName: request.body.timeFieldName, - type: KBN_FIELD_TYPES.DATE, - interval: overallTimeSeries.interval, - min: overallTimeSeries.stats[0], - max: overallTimeSeries.stats[1], - }, - ], - // samplerShardSize - -1, - undefined - )) as [NumericChartData]; + logInfoMessage('Fetch group histograms.'); - const histogram = - overallTimeSeries.data.map((o, i) => { - const current = cpgTimeSeries.data.find( - (d1) => d1.key_as_string === o.key_as_string - ) ?? { - doc_count: 0, - }; - return { - key: o.key, - key_as_string: o.key_as_string ?? '', - doc_count_change_point: current.doc_count, - doc_count_overall: Math.max(0, o.doc_count - current.doc_count), + await asyncForEach(changePointGroups, async (cpg) => { + if (overallTimeSeries !== undefined) { + const histogramQuery = { + bool: { + filter: cpg.group.map((d) => ({ + term: { [d.fieldName]: d.fieldValue }, + })), + }, }; - }) ?? []; - push( - addChangePointsGroupHistogramAction([ - { - id: cpg.id, - histogram, - }, - ]) - ); - }); + let cpgTimeSeries: NumericChartData; + try { + cpgTimeSeries = ( + (await fetchHistogramsForFields( + client, + request.body.index, + histogramQuery, + // fields + [ + { + fieldName: request.body.timeFieldName, + type: KBN_FIELD_TYPES.DATE, + interval: overallTimeSeries.interval, + min: overallTimeSeries.stats[0], + max: overallTimeSeries.stats[1], + }, + ], + // samplerShardSize + -1, + undefined + )) as [NumericChartData] + )[0]; + } catch (e) { + logger.error( + `Failed to fetch the histogram data for group #${ + cpg.id + }, got: \n${e.toString()}` + ); + pushError(`Failed to fetch the histogram data for group #${cpg.id}.`); + return; + } + const histogram = + overallTimeSeries.data.map((o, i) => { + const current = cpgTimeSeries.data.find( + (d1) => d1.key_as_string === o.key_as_string + ) ?? { + doc_count: 0, + }; + return { + key: o.key, + key_as_string: o.key_as_string ?? '', + doc_count_change_point: current.doc_count, + doc_count_overall: Math.max(0, o.doc_count - current.doc_count), + }; + }) ?? []; + + push( + addChangePointsGroupHistogramAction([ + { + id: cpg.id, + histogram, + }, + ]) + ); + } + }); + } + } catch (e) { + logger.error( + `Failed to transform field/value pairs into groups, got: \n${e.toString()}` + ); + pushError(`Failed to transform field/value pairs into groups.`); } } loaded += PROGRESS_STEP_HISTOGRAMS_GROUPS; + logInfoMessage('Fetch field/value histograms.'); + // time series filtered by fields - if (changePoints) { - await asyncForEach(changePoints, async (cp, index) => { - if (changePoints) { + if (changePoints && overallTimeSeries !== undefined) { + await asyncForEach(changePoints, async (cp) => { + if (overallTimeSeries !== undefined) { const histogramQuery = { bool: { filter: [ @@ -495,24 +593,40 @@ export const defineExplainLogRateSpikesRoute = ( }, }; - const [cpTimeSeries] = (await fetchHistogramsForFields( - client, - request.body.index, - histogramQuery, - // fields - [ - { - fieldName: request.body.timeFieldName, - type: KBN_FIELD_TYPES.DATE, - interval: overallTimeSeries.interval, - min: overallTimeSeries.stats[0], - max: overallTimeSeries.stats[1], - }, - ], - // samplerShardSize - -1, - undefined - )) as [NumericChartData]; + let cpTimeSeries: NumericChartData; + + try { + cpTimeSeries = ( + (await fetchHistogramsForFields( + client, + request.body.index, + histogramQuery, + // fields + [ + { + fieldName: request.body.timeFieldName, + type: KBN_FIELD_TYPES.DATE, + interval: overallTimeSeries.interval, + min: overallTimeSeries.stats[0], + max: overallTimeSeries.stats[1], + }, + ], + // samplerShardSize + -1, + undefined + )) as [NumericChartData] + )[0]; + } catch (e) { + logger.error( + `Failed to fetch the histogram data for field/value pair "${cp.fieldName}:${ + cp.fieldValue + }", got: \n${e.toString()}` + ); + pushError( + `Failed to fetch the histogram data for field/value pair "${cp.fieldName}:${cp.fieldValue}".` + ); + return; + } const histogram = overallTimeSeries.data.map((o, i) => { diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts index 03242a4bc8ae..0fb7f90c89c1 100644 --- a/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts +++ b/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts @@ -8,6 +8,7 @@ import { uniqBy } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; import { ChangePoint } from '@kbn/ml-agg-utils'; import { SPIKE_ANALYSIS_THRESHOLD } from '../../../common/constants'; import type { AiopsExplainLogRateSpikesSchema } from '../../../common/api/explain_log_rate_spikes'; @@ -92,7 +93,9 @@ interface Aggs extends estypes.AggregationsSignificantLongTermsAggregate { export const fetchChangePointPValues = async ( esClient: ElasticsearchClient, params: AiopsExplainLogRateSpikesSchema, - fieldNames: string[] + fieldNames: string[], + logger: Logger, + emitError: (m: string) => void ): Promise => { const result: ChangePoint[] = []; @@ -101,7 +104,16 @@ export const fetchChangePointPValues = async ( const resp = await esClient.search(request); if (resp.aggregations === undefined) { - throw new Error('fetchChangePoint failed, did not return aggregations.'); + logger.error( + `Failed to fetch p-value aggregation for fieldName "${fieldName}", got: \n${JSON.stringify( + resp, + null, + 2 + )}` + ); + emitError(`Failed to fetch p-value aggregation for fieldName "${fieldName}".`); + // Still continue the analysis even if individual p-value queries fail. + continue; } const overallResult = resp.aggregations.change_point_p_value; diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts index 055c22397064..c9444aaca22a 100644 --- a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts +++ b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts @@ -10,6 +10,7 @@ import { uniq, uniqWith, pick, isEqual } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { Logger } from '@kbn/logging'; import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils'; interface FrequentItemsAggregation extends estypes.AggregationsSamplerAggregation { @@ -53,9 +54,11 @@ export async function fetchFrequentItems( changePoints: ChangePoint[], timeFieldName: string, deviationMin: number, - deviationMax: number + deviationMax: number, + logger: Logger, + emitError: (m: string) => void ) { - // get unique fields that are left + // get unique fields from change points const fields = [...new Set(changePoints.map((t) => t.fieldName))]; // TODO add query params @@ -91,6 +94,8 @@ export async function fetchFrequentItems( sampleProbability = Math.min(0.5, minDocCount / totalDocCount); } + logger.debug(`frequent_items sample probability: ${sampleProbability}`); + // frequent items can be slow, so sample and use 10% min_support const aggs: Record = { sample: { @@ -103,7 +108,7 @@ export async function fetchFrequentItems( frequent_items: { minimum_set_size: 2, size: 200, - minimum_support: 0.01, + minimum_support: 0.1, fields: aggFields, }, }, @@ -125,12 +130,18 @@ export async function fetchFrequentItems( { maxRetries: 0 } ); - const totalDocCountFi = (body.hits.total as estypes.SearchTotalHits).value; - if (body.aggregations === undefined) { - throw new Error('fetchFrequentItems failed, did not return aggregations.'); + logger.error(`Failed to fetch frequent_items, got: \n${JSON.stringify(body, null, 2)}`); + emitError(`Failed to fetch frequent_items.`); + return { + fields: [], + df: [], + totalDocCount: 0, + }; } + const totalDocCountFi = (body.hits.total as estypes.SearchTotalHits).value; + const shape = body.aggregations.sample.fi.buckets.length; let maximum = shape; if (maximum > 50000) { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts index d1159efd0fc9..7d40105db192 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts @@ -36,19 +36,19 @@ describe('Comparison feature flag', () => { it('shows the comparison feature enabled in services overview', () => { cy.visitKibana('/app/apm/services'); cy.get('input[type="checkbox"]#comparison').should('be.checked'); - cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled'); + cy.getByTestSubj('comparisonSelect').should('not.be.disabled'); }); it('shows the comparison feature enabled in dependencies overview', () => { cy.visitKibana('/app/apm/dependencies'); cy.get('input[type="checkbox"]#comparison').should('be.checked'); - cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled'); + cy.getByTestSubj('comparisonSelect').should('not.be.disabled'); }); it('shows the comparison feature disabled in service map overview page', () => { cy.visitKibana('/app/apm/service-map'); cy.get('input[type="checkbox"]#comparison').should('be.checked'); - cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled'); + cy.getByTestSubj('comparisonSelect').should('not.be.disabled'); }); }); @@ -71,7 +71,7 @@ describe('Comparison feature flag', () => { it('shows the comparison feature disabled in services overview', () => { cy.visitKibana('/app/apm/services'); cy.get('input[type="checkbox"]#comparison').should('not.be.checked'); - cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); + cy.getByTestSubj('comparisonSelect').should('be.disabled'); }); it('shows the comparison feature disabled in dependencies overview page', () => { @@ -81,13 +81,13 @@ describe('Comparison feature flag', () => { cy.visitKibana('/app/apm/dependencies'); cy.wait('@topDependenciesRequest'); cy.get('input[type="checkbox"]#comparison').should('not.be.checked'); - cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); + cy.getByTestSubj('comparisonSelect').should('be.disabled'); }); it('shows the comparison feature disabled in service map overview page', () => { cy.visitKibana('/app/apm/service-map'); cy.get('input[type="checkbox"]#comparison').should('not.be.checked'); - cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); + cy.getByTestSubj('comparisonSelect').should('be.disabled'); }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts index c25e6a680031..5d275770e462 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts @@ -60,21 +60,19 @@ describe('when navigating to integration page', () => { cy.visitKibana(integrationsPath); // open integration policy form - cy.get('[data-test-subj="integration-card:epr:apm:featured').click(); + cy.getByTestSubj('integration-card:epr:apm:featured').click(); cy.contains('Elastic APM in Fleet').click(); cy.contains('a', 'APM integration').click(); - cy.get('[data-test-subj="addIntegrationPolicyButton"]').click(); + cy.getByTestSubj('addIntegrationPolicyButton').click(); }); it('checks validators for required fields', () => { const requiredFields = policyFormFields.filter((field) => field.required); requiredFields.map((field) => { - cy.get(`[data-test-subj="${field.selector}"`).clear(); - cy.get('[data-test-subj="createPackagePolicySaveButton"').should( - 'be.disabled' - ); - cy.get(`[data-test-subj="${field.selector}"`).type(field.value); + cy.getByTestSubj(field.selector).clear(); + cy.getByTestSubj('createPackagePolicySaveButton').should('be.disabled'); + cy.getByTestSubj(field.selector).type(field.value); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts index 5be39b4f082d..47f8c537b100 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts @@ -90,7 +90,7 @@ describe('Agent configuration', () => { '/api/apm/settings/agent-configuration/environments?*' ).as('serviceEnvironmentApi'); cy.contains('Create configuration').click(); - cy.get('[data-test-subj="serviceNameComboBox"]') + cy.getByTestSubj('serviceNameComboBox') .click() .type('opbeans-node') .type('{enter}'); @@ -98,7 +98,7 @@ describe('Agent configuration', () => { cy.contains('opbeans-node').realClick(); cy.wait('@serviceEnvironmentApi'); - cy.get('[data-test-subj="serviceEnviromentComboBox"]') + cy.getByTestSubj('serviceEnviromentComboBox') .click({ force: true }) .type('prod') .type('{enter}'); @@ -115,14 +115,11 @@ describe('Agent configuration', () => { '/api/apm/settings/agent-configuration/environments' ).as('serviceEnvironmentApi'); cy.contains('Create configuration').click(); - cy.get('[data-test-subj="serviceNameComboBox"]') - .click() - .type('All') - .type('{enter}'); + cy.getByTestSubj('serviceNameComboBox').click().type('All').type('{enter}'); cy.contains('All').realClick(); cy.wait('@serviceEnvironmentApi'); - cy.get('[data-test-subj="serviceEnviromentComboBox"]') + cy.getByTestSubj('serviceEnviromentComboBox') .click({ force: true }) .type('All'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts index 615ff2b49a85..b680f745609b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts @@ -52,7 +52,7 @@ describe('Custom links', () => { it('creates custom link', () => { cy.visitKibana(basePath); - const emptyPrompt = cy.get('[data-test-subj="customLinksEmptyPrompt"]'); + const emptyPrompt = cy.getByTestSubj('customLinksEmptyPrompt'); cy.contains('Create custom link').click(); cy.contains('Create link'); cy.contains('Save').should('be.disabled'); @@ -63,7 +63,7 @@ describe('Custom links', () => { emptyPrompt.should('not.exist'); cy.contains('foo'); cy.contains('https://foo.com'); - cy.get('[data-test-subj="editCustomLink"]').click(); + cy.getByTestSubj('editCustomLink').click(); cy.contains('Delete').click(); }); @@ -71,14 +71,14 @@ describe('Custom links', () => { cy.visitKibana(basePath); // wait for empty prompt - cy.get('[data-test-subj="customLinksEmptyPrompt"]').should('be.visible'); + cy.getByTestSubj('customLinksEmptyPrompt').should('be.visible'); cy.contains('Create custom link').click(); - cy.get('[data-test-subj="filter-0"]').select('service.name'); + cy.getByTestSubj('filter-0').select('service.name'); cy.get( '[data-test-subj="service.name.value"] [data-test-subj="comboBoxSearchInput"]' ).type('foo'); - cy.get('[data-test-subj="filter-0"]').select('service.environment'); + cy.getByTestSubj('filter-0').select('service.environment'); cy.get( '[data-test-subj="service.environment.value"] [data-test-subj="comboBoxInput"]' ).should('not.contain', 'foo'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts index e989ea5cf0fa..20577f8bf579 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts @@ -85,7 +85,7 @@ describe('Storage Explorer', () => { }); it('renders the storage timeseries chart', () => { - cy.get('[data-test-subj="storageExplorerTimeseriesChart"]'); + cy.getByTestSubj('storageExplorerTimeseriesChart'); }); it('has a list of services and environments', () => { @@ -115,7 +115,7 @@ describe('Storage Explorer', () => { it('with the correct environment when changing the environment', () => { cy.wait(mainAliasNames); - cy.get('[data-test-subj="environmentFilter"]').type('production'); + cy.getByTestSubj('environmentFilter').type('production'); cy.contains('button', 'production').click({ force: true }); @@ -148,7 +148,7 @@ describe('Storage Explorer', () => { it('with the correct lifecycle phase when changing the lifecycle phase', () => { cy.wait(mainAliasNames); - cy.get('[data-test-subj="storageExplorerLifecyclePhaseSelect"]').click(); + cy.getByTestSubj('storageExplorerLifecyclePhaseSelect').click(); cy.contains('button', 'Warm').click(); cy.expectAPIsToHaveBeenCalledWith({ @@ -180,13 +180,13 @@ describe('Storage Explorer', () => { cy.wait(mainAliasNames); cy.contains('opbeans-node'); - cy.get('[data-test-subj="storageDetailsButton_opbeans-node"]').click(); - cy.get('[data-test-subj="loadingSpinner"]').should('be.visible'); + cy.getByTestSubj('storageDetailsButton_opbeans-node').click(); + cy.getByTestSubj('loadingSpinner').should('be.visible'); cy.wait('@storageDetailsRequest'); cy.contains('Service storage details'); - cy.get('[data-test-subj="storageExplorerTimeseriesChart"]'); - cy.get('[data-test-subj="serviceStorageDetailsTable"]'); + cy.getByTestSubj('storageExplorerTimeseriesChart'); + cy.getByTestSubj('serviceStorageDetailsTable'); }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts index cfcabe85b5b2..00b842f3265c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts @@ -11,7 +11,7 @@ describe('APM deep links', () => { }); it('navigates to apm links on search elastic', () => { cy.visitKibana('/'); - cy.get('[data-test-subj="nav-search-input"]').type('APM'); + cy.getByTestSubj('nav-search-input').type('APM'); cy.contains('APM'); cy.contains('APM / Services'); cy.contains('APM / Traces'); @@ -23,17 +23,17 @@ describe('APM deep links', () => { cy.contains('APM').click({ force: true }); cy.url().should('include', '/apm/services'); - cy.get('[data-test-subj="nav-search-input"]').type('APM'); + cy.getByTestSubj('nav-search-input').type('APM'); // navigates to services page cy.contains('APM / Services').click({ force: true }); cy.url().should('include', '/apm/services'); - cy.get('[data-test-subj="nav-search-input"]').type('APM'); + cy.getByTestSubj('nav-search-input').type('APM'); // navigates to traces page cy.contains('APM / Traces').click({ force: true }); cy.url().should('include', '/apm/traces'); - cy.get('[data-test-subj="nav-search-input"]').type('APM'); + cy.getByTestSubj('nav-search-input').type('APM'); // navigates to service maps cy.contains('APM / Service Map').click({ force: true }); cy.url().should('include', '/apm/service-map'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts index 653809a8e04d..2ef3ae42b1aa 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts @@ -66,9 +66,9 @@ describe('Dependencies', () => { })}` ); - cy.get('[data-test-subj="latencyChart"]'); - cy.get('[data-test-subj="throughputChart"]'); - cy.get('[data-test-subj="errorRateChart"]'); + cy.getByTestSubj('latencyChart'); + cy.getByTestSubj('throughputChart'); + cy.getByTestSubj('errorRateChart'); cy.contains('opbeans-java').click({ force: true }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts index 19de523c7ab1..d00d8036df3b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts @@ -68,13 +68,13 @@ describe('Error details', () => { it('shows errors distribution chart', () => { cy.visitKibana(errorDetailsPageHref); cy.contains('Error group 00000'); - cy.get('[data-test-subj="errorDistribution"]').contains('Occurrences'); + cy.getByTestSubj('errorDistribution').contains('Occurrences'); }); it('shows top erroneous transactions table', () => { cy.visitKibana(errorDetailsPageHref); cy.contains('Top 5 affected transactions'); - cy.get('[data-test-subj="topErroneousTransactionsTable"]') + cy.getByTestSubj('topErroneousTransactionsTable') .contains('a', 'GET /apple 🍎') .click(); cy.url().should('include', 'opbeans-java/transactions/view'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts index 301b3384ee2e..8ac95d509d0b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts @@ -81,14 +81,14 @@ describe('Errors page', () => { it('clicking on type adds a filter in the kuerybar', () => { cy.visitKibana(javaServiceErrorsPageHref); - cy.get('[data-test-subj="headerFilterKuerybar"]') + cy.getByTestSubj('headerFilterKuerybar') .invoke('val') .should('be.empty'); // `force: true` because Cypress says the element is 0x0 cy.contains('exception 0').click({ force: true, }); - cy.get('[data-test-subj="headerFilterKuerybar"]') + cy.getByTestSubj('headerFilterKuerybar') .its('length') .should('be.gt', 0); cy.get('table') @@ -158,7 +158,7 @@ describe('Check detailed statistics API with multiple errors', () => { ]) ); }); - cy.get('[data-test-subj="pagination-button-1"]').click(); + cy.getByTestSubj('pagination-button-1').click(); cy.wait('@errorsDetailedStatistics').then((payload) => { expect(payload.request.body.groupIds).eql( JSON.stringify([ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts index 2ee2f4f019b1..e0c4a3aedd2b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts @@ -69,7 +69,7 @@ describe('Home page', () => { cy.contains('Services'); cy.contains('opbeans-rum').click({ force: true }); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'page-load' ); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts index c4e87ac15fbe..4f72e968d81f 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts @@ -44,7 +44,7 @@ describe('Service inventory - header filters', () => { cy.contains('Services'); cy.contains('opbeans-node'); cy.contains('service 1'); - cy.get('[data-test-subj="headerFilterKuerybar"]') + cy.getByTestSubj('headerFilterKuerybar') .type(`service.name: "${specialServiceName}"`) .type('{enter}'); cy.contains('service 1'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts index 015df91d792e..2d40c690a8c9 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts @@ -93,7 +93,7 @@ describe('Service inventory', () => { it('with the correct environment when changing the environment', () => { cy.wait(mainAliasNames); - cy.get('[data-test-subj="environmentFilter"]').type('production'); + cy.getByTestSubj('environmentFilter').type('production'); cy.contains('button', 'production').click(); @@ -175,7 +175,7 @@ describe('Service inventory', () => { ]) ); }); - cy.get('[data-test-subj="pagination-button-1"]').click(); + cy.getByTestSubj('pagination-button-1').click(); cy.wait('@detailedStatisticsRequest').then((payload) => { expect(payload.request.body.serviceNames).eql( JSON.stringify([ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts index b175eb0430ed..d693148010c7 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts @@ -50,16 +50,12 @@ describe('Errors table', () => { it('clicking on type adds a filter in the kuerybar and navigates to errors page', () => { cy.visitKibana(serviceOverviewHref); - cy.get('[data-test-subj="headerFilterKuerybar"]') - .invoke('val') - .should('be.empty'); + cy.getByTestSubj('headerFilterKuerybar').invoke('val').should('be.empty'); // `force: true` because Cypress says the element is 0x0 cy.contains('Exception').click({ force: true, }); - cy.get('[data-test-subj="headerFilterKuerybar"]') - .its('length') - .should('be.gt', 0); + cy.getByTestSubj('headerFilterKuerybar').its('length').should('be.gt', 0); cy.get('table').find('td:contains("Exception")').should('have.length', 1); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts index 6376d544821a..8a2502450669 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts @@ -77,13 +77,13 @@ describe('Service overview - header filters', () => { cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-node'); cy.url().should('not.include', 'transactionType'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'request' ); - cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); + cy.getByTestSubj('headerFilterTransactionType').select('Worker'); cy.url().should('include', 'transactionType=Worker'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); @@ -94,7 +94,7 @@ describe('Service overview - header filters', () => { cy.intercept('GET', endpoint).as(name); }); cy.visitKibana(serviceOverviewHref); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'request' ); @@ -104,9 +104,9 @@ describe('Service overview - header filters', () => { value: 'transactionType=request', }); - cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); + cy.getByTestSubj('headerFilterTransactionType').select('Worker'); cy.url().should('include', 'transactionType=Worker'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); @@ -129,18 +129,12 @@ describe('Service overview - header filters', () => { }) ); cy.contains('opbeans-java'); - cy.get('[data-test-subj="headerFilterKuerybar"]').type('transaction.n'); + cy.getByTestSubj('headerFilterKuerybar').type('transaction.n'); cy.contains('transaction.name'); - cy.get('[data-test-subj="suggestionContainer"]') - .find('li') - .first() - .click(); - cy.get('[data-test-subj="headerFilterKuerybar"]').type(':'); - cy.get('[data-test-subj="suggestionContainer"]') - .find('li') - .first() - .click(); - cy.get('[data-test-subj="headerFilterKuerybar"]').type('{enter}'); + cy.getByTestSubj('suggestionContainer').find('li').first().click(); + cy.getByTestSubj('headerFilterKuerybar').type(':'); + cy.getByTestSubj('suggestionContainer').find('li').first().click(); + cy.getByTestSubj('headerFilterKuerybar').type('{enter}'); cy.url().should('include', '&kuery=transaction.name'); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts index 03653df2b0bb..578b116a1059 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts @@ -63,7 +63,7 @@ describe('Instances table', () => { it('shows empty message', () => { cy.visitKibana(testServiveHref); cy.contains('test-service'); - cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains( + cy.getByTestSubj('serviceInstancesTableContainer').contains( 'No instances found' ); }); @@ -77,9 +77,7 @@ describe('Instances table', () => { it('hides instances table', () => { cy.visitKibana(serviceRumOverviewHref); cy.contains('opbeans-rum'); - cy.get('[data-test-subj="serviceInstancesTableContainer"]').should( - 'not.exist' - ); + cy.getByTestSubj('serviceInstancesTableContainer').should('not.exist'); }); }); @@ -109,10 +107,8 @@ describe('Instances table', () => { cy.contains(serviceNodeName); cy.wait('@instancesDetailsRequest'); - cy.get( - `[data-test-subj="instanceDetailsButton_${serviceNodeName}"]` - ).realClick(); - cy.get('[data-test-subj="loadingSpinner"]').should('be.visible'); + cy.getByTestSubj(`instanceDetailsButton_${serviceNodeName}`).realClick(); + cy.getByTestSubj('loadingSpinner').should('be.visible'); cy.wait('@instanceDetailsRequest').then(() => { cy.contains('Service'); }); @@ -130,9 +126,7 @@ describe('Instances table', () => { cy.contains(serviceNodeName); cy.wait('@instancesDetailsRequest'); - cy.get( - `[data-test-subj="instanceActionsButton_${serviceNodeName}"]` - ).click(); + cy.getByTestSubj(`instanceActionsButton_${serviceNodeName}`).click(); cy.contains('Pod logs'); cy.contains('Pod metrics'); // cy.contains('Container logs'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts index e8319c8efafe..8173e94557b2 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts @@ -109,13 +109,13 @@ describe('Service Overview', () => { cy.contains('opbeans-node'); // set skipFailures to true to not fail the test when there are accessibility failures checkA11y({ skipFailures: true }); - cy.get('[data-test-subj="latencyChart"]'); - cy.get('[data-test-subj="throughput"]'); - cy.get('[data-test-subj="transactionsGroupTable"]'); - cy.get('[data-test-subj="serviceOverviewErrorsTable"]'); - cy.get('[data-test-subj="dependenciesTable"]'); - cy.get('[data-test-subj="instancesLatencyDistribution"]'); - cy.get('[data-test-subj="serviceOverviewInstancesTable"]'); + cy.getByTestSubj('latencyChart'); + cy.getByTestSubj('throughput'); + cy.getByTestSubj('transactionsGroupTable'); + cy.getByTestSubj('serviceOverviewErrorsTable'); + cy.getByTestSubj('dependenciesTable'); + cy.getByTestSubj('instancesLatencyDistribution'); + cy.getByTestSubj('serviceOverviewInstancesTable'); }); }); @@ -134,17 +134,17 @@ describe('Service Overview', () => { cy.wait('@transactionTypesRequest'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'request' ); - cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').select('Worker'); + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); cy.contains('Transactions').click(); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); @@ -159,18 +159,18 @@ describe('Service Overview', () => { cy.visitKibana(baseUrl); cy.wait('@transactionTypesRequest'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'request' ); - cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').select('Worker'); + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); cy.contains('View transactions').click(); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); @@ -226,7 +226,7 @@ describe('Service Overview', () => { 'suggestionsRequest' ); - cy.get('[data-test-subj="environmentFilter"] input').type('production', { + cy.getByTestSubj('environmentFilter').find('input').type('production', { force: true, }); @@ -235,9 +235,7 @@ describe('Service Overview', () => { value: 'fieldValue=production', }); - cy.get( - '[data-test-subj="comboBoxOptionsList environmentFilter-optionsList"]' - ) + cy.getByTestSubj('comboBoxOptionsList environmentFilter-optionsList') .contains('production') .click({ force: true }); @@ -271,11 +269,11 @@ describe('Service Overview', () => { }); it('when selecting a different comparison window', () => { - cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d'); + cy.getByTestSubj('comparisonSelect').should('have.value', '1d'); // selects another comparison type - cy.get('[data-test-subj="comparisonSelect"]').select('1w'); - cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1w'); + cy.getByTestSubj('comparisonSelect').select('1w'); + cy.getByTestSubj('comparisonSelect').should('have.value', '1w'); cy.expectAPIsToHaveBeenCalledWith({ apisIntercepted: aliasNamesWithComparison, value: 'offset', diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts index 718a2a4a06cf..bce3da42d5a3 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts @@ -101,18 +101,18 @@ describe('Service overview: Time Comparison', () => { cy.visitKibana(serviceOverviewPath); cy.contains('opbeans-java'); // opens the page with "Day before" selected - cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d'); + cy.getByTestSubj('comparisonSelect').should('have.value', '1d'); // selects another comparison type - cy.get('[data-test-subj="comparisonSelect"]').select('1w'); - cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1w'); + cy.getByTestSubj('comparisonSelect').select('1w'); + cy.getByTestSubj('comparisonSelect').should('have.value', '1w'); }); it('changes comparison type when a new time range is selected', () => { cy.visitKibana(serviceOverviewHref); cy.contains('opbeans-java'); // Time comparison default value - cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d'); + cy.getByTestSubj('comparisonSelect').should('have.value', '1d'); cy.contains('Day before'); cy.contains('Week before'); @@ -121,17 +121,14 @@ describe('Service overview: Time Comparison', () => { '2021-10-20T00:00:00.000Z' ); - cy.get('[data-test-subj="superDatePickerApplyTimeButton"]').click(); + cy.getByTestSubj('superDatePickerApplyTimeButton').click(); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'have.value', - '864000000ms' - ); - cy.get('[data-test-subj="comparisonSelect"]').should( + cy.getByTestSubj('comparisonSelect').should('have.value', '864000000ms'); + cy.getByTestSubj('comparisonSelect').should( 'not.contain.text', 'Day before' ); - cy.get('[data-test-subj="comparisonSelect"]').should( + cy.getByTestSubj('comparisonSelect').should( 'not.contain.text', 'Week before' ); @@ -141,17 +138,14 @@ describe('Service overview: Time Comparison', () => { cy.contains('Week before'); cy.changeTimeRange('Last 24 hours'); - cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d'); + cy.getByTestSubj('comparisonSelect').should('have.value', '1d'); cy.contains('Day before'); cy.contains('Week before'); cy.changeTimeRange('Last 7 days'); - cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1w'); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'contain.text', - 'Week before' - ); - cy.get('[data-test-subj="comparisonSelect"]').should( + cy.getByTestSubj('comparisonSelect').should('have.value', '1w'); + cy.getByTestSubj('comparisonSelect').should('contain.text', 'Week before'); + cy.getByTestSubj('comparisonSelect').should( 'not.contain.text', 'Day before' ); @@ -170,7 +164,7 @@ describe('Service overview: Time Comparison', () => { ); cy.contains('opbeans-java'); cy.wait('@throughputChartRequest'); - cy.get('[data-test-subj="throughput"]') + cy.getByTestSubj('throughput') .get('#echHighlighterClipPath__throughput') .realHover({ position: 'center' }); cy.contains('Week before'); @@ -186,17 +180,17 @@ describe('Service overview: Time Comparison', () => { cy.contains('opbeans-java'); // Comparison is enabled by default - cy.get('[data-test-subj="comparisonSelect"]').should('be.enabled'); + cy.getByTestSubj('comparisonSelect').should('be.enabled'); // toggles off comparison cy.contains('Comparison').click(); - cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); + cy.getByTestSubj('comparisonSelect').should('be.disabled'); }); it('calls APIs without comparison time range', () => { cy.visitKibana(serviceOverviewHref); - cy.get('[data-test-subj="comparisonSelect"]').should('be.enabled'); + cy.getByTestSubj('comparisonSelect').should('be.enabled'); const offset = `offset=1d`; // When the page loads it fetches all APIs with comparison time range @@ -212,7 +206,7 @@ describe('Service overview: Time Comparison', () => { // toggles off comparison cy.contains('Comparison').click(); - cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); + cy.getByTestSubj('comparisonSelect').should('be.disabled'); // When comparison is disabled APIs are called withou comparison time range cy.wait(apisToIntercept.map(({ name }) => `@${name}`)).then( (interceptions) => { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts index cddba048e8a1..60b36b10ee4a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts @@ -50,8 +50,8 @@ describe('Span links', () => { ); cy.contains('Transaction A').click(); cy.contains('2 Span links'); - cy.get( - `[data-test-subj="spanLinksBadge_${ids.producerInternalOnlyIds.spanAId}"]` + cy.getByTestSubj( + `spanLinksBadge_${ids.producerInternalOnlyIds.spanAId}` ).realHover(); cy.contains('2 Span links found'); cy.contains('2 incoming'); @@ -64,8 +64,8 @@ describe('Span links', () => { ); cy.contains('Transaction B').click(); cy.contains('2 Span links'); - cy.get( - `[data-test-subj="spanLinksBadge_${ids.producerExternalOnlyIds.spanBId}"]` + cy.getByTestSubj( + `spanLinksBadge_${ids.producerExternalOnlyIds.spanBId}` ).realHover(); cy.contains('2 Span links found'); cy.contains('1 incoming'); @@ -78,8 +78,8 @@ describe('Span links', () => { ); cy.contains('Transaction C').click(); cy.contains('2 Span links'); - cy.get( - `[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.transactionCId}"]` + cy.getByTestSubj( + `spanLinksBadge_${ids.producerConsumerIds.transactionCId}` ).realHover(); cy.contains('2 Span links found'); cy.contains('1 incoming'); @@ -92,8 +92,8 @@ describe('Span links', () => { ); cy.contains('Transaction C').click(); cy.contains('1 Span link'); - cy.get( - `[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.spanCId}"]` + cy.getByTestSubj( + `spanLinksBadge_${ids.producerConsumerIds.spanCId}` ).realHover(); cy.contains('1 Span link found'); cy.contains('1 incoming'); @@ -106,8 +106,8 @@ describe('Span links', () => { ); cy.contains('Transaction D').click(); cy.contains('2 Span links'); - cy.get( - `[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.transactionDId}"]` + cy.getByTestSubj( + `spanLinksBadge_${ids.producerMultipleIds.transactionDId}` ).realHover(); cy.contains('2 Span links found'); cy.contains('0 incoming'); @@ -120,8 +120,8 @@ describe('Span links', () => { ); cy.contains('Transaction D').click(); cy.contains('2 Span links'); - cy.get( - `[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.spanEId}"]` + cy.getByTestSubj( + `spanLinksBadge_${ids.producerMultipleIds.spanEId}` ).realHover(); cy.contains('2 Span links found'); cy.contains('0 incoming'); @@ -136,7 +136,7 @@ describe('Span links', () => { ); cy.contains('Transaction A').click(); cy.contains('Span A').click(); - cy.get('[data-test-subj="spanLinksTab"]').click(); + cy.getByTestSubj('spanLinksTab').click(); cy.contains('producer-consumer') .should('have.attr', 'href') .and('include', '/services/producer-consumer/overview'); @@ -155,7 +155,7 @@ describe('Span links', () => { 'include', `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}` ); - cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + cy.getByTestSubj('spanLinkTypeSelect').should( 'contain.text', 'Outgoing links (0)' ); @@ -167,7 +167,7 @@ describe('Span links', () => { ); cy.contains('Transaction B').click(); cy.contains('Span B').click(); - cy.get('[data-test-subj="spanLinksTab"]').click(); + cy.getByTestSubj('spanLinksTab').click(); cy.contains('consumer-multiple') .should('have.attr', 'href') @@ -178,9 +178,7 @@ describe('Span links', () => { 'include', `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}` ); - cy.get('[data-test-subj="spanLinkTypeSelect"]').select( - 'Outgoing links (1)' - ); + cy.getByTestSubj('spanLinkTypeSelect').select('Outgoing links (1)'); cy.contains('Unknown'); cy.contains('trace#1-span#1'); }); @@ -193,7 +191,7 @@ describe('Span links', () => { cy.get( `[aria-controls="${ids.producerConsumerIds.transactionCId}"]` ).click(); - cy.get('[data-test-subj="spanLinksTab"]').click(); + cy.getByTestSubj('spanLinksTab').click(); cy.contains('consumer-multiple') .should('have.attr', 'href') @@ -205,9 +203,7 @@ describe('Span links', () => { `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}` ); - cy.get('[data-test-subj="spanLinkTypeSelect"]').select( - 'Outgoing links (1)' - ); + cy.getByTestSubj('spanLinkTypeSelect').select('Outgoing links (1)'); cy.contains('producer-internal-only') .should('have.attr', 'href') .and('include', '/services/producer-internal-only/overview'); @@ -225,7 +221,7 @@ describe('Span links', () => { ); cy.contains('Transaction C').click(); cy.contains('Span C').click(); - cy.get('[data-test-subj="spanLinksTab"]').click(); + cy.getByTestSubj('spanLinksTab').click(); cy.contains('consumer-multiple') .should('have.attr', 'href') @@ -237,7 +233,7 @@ describe('Span links', () => { `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}` ); - cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + cy.getByTestSubj('spanLinkTypeSelect').should( 'contain.text', 'Outgoing links (0)' ); @@ -251,7 +247,7 @@ describe('Span links', () => { cy.get( `[aria-controls="${ids.producerMultipleIds.transactionDId}"]` ).click(); - cy.get('[data-test-subj="spanLinksTab"]').click(); + cy.getByTestSubj('spanLinksTab').click(); cy.contains('producer-consumer') .should('have.attr', 'href') @@ -273,7 +269,7 @@ describe('Span links', () => { `link-to/transaction/${ids.producerInternalOnlyIds.transactionAId}?waterfallItemId=${ids.producerInternalOnlyIds.spanAId}` ); - cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + cy.getByTestSubj('spanLinkTypeSelect').should( 'contain.text', 'Incoming links (0)' ); @@ -285,7 +281,7 @@ describe('Span links', () => { ); cy.contains('Transaction D').click(); cy.contains('Span E').click(); - cy.get('[data-test-subj="spanLinksTab"]').click(); + cy.getByTestSubj('spanLinksTab').click(); cy.contains('producer-external-only') .should('have.attr', 'href') @@ -307,7 +303,7 @@ describe('Span links', () => { `link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.transactionCId}` ); - cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + cy.getByTestSubj('spanLinkTypeSelect').should( 'contain.text', 'Incoming links (0)' ); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts index 5172a5f167fc..09bd37f5b0b6 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts @@ -42,15 +42,15 @@ describe('Transaction details', () => { it('shows transaction name and transaction charts', () => { cy.contains('h2', 'GET /api/product'); - cy.get('[data-test-subj="latencyChart"]'); - cy.get('[data-test-subj="throughput"]'); - cy.get('[data-test-subj="transactionBreakdownChart"]'); - cy.get('[data-test-subj="errorRate"]'); + cy.getByTestSubj('latencyChart'); + cy.getByTestSubj('throughput'); + cy.getByTestSubj('transactionBreakdownChart'); + cy.getByTestSubj('errorRate'); }); it('shows top errors table', () => { cy.contains('Top 5 errors'); - cy.get('[data-test-subj="topErrorsForTransactionTable"]') + cy.getByTestSubj('topErrorsForTransactionTable') .contains('a', '[MockError] Foo') .click(); cy.url().should('include', 'opbeans-java/errors'); @@ -58,7 +58,7 @@ describe('Transaction details', () => { describe('when navigating to a trace sample', () => { it('keeps the same trace sample after reloading the page', () => { - cy.get('[data-test-subj="pagination-button-last"]').click(); + cy.getByTestSubj('pagination-button-last').click(); cy.url().then((url) => { cy.reload(); cy.url().should('eq', url); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts index 83753b7fe259..2e7e0d336cd5 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts @@ -49,17 +49,17 @@ describe('Transactions Overview', () => { it('persists transaction type selected when navigating to Overview tab', () => { cy.visitKibana(serviceTransactionsHref); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'request' ); - cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').select('Worker'); + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); cy.get('a[href*="/app/apm/services/opbeans-node/overview"]').click(); - cy.get('[data-test-subj="headerFilterTransactionType"]').should( + cy.getByTestSubj('headerFilterTransactionType').should( 'have.value', 'Worker' ); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 7830e791c365..9e6e0189e636 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -52,15 +52,19 @@ Cypress.Commands.add( } ); +Cypress.Commands.add('getByTestSubj', (selector: string) => { + return cy.get(`[data-test-subj="${selector}"]`); +}); + Cypress.Commands.add('changeTimeRange', (value: string) => { - cy.get('[data-test-subj="superDatePickerToggleQuickMenuButton"]').click(); + cy.getByTestSubj('superDatePickerToggleQuickMenuButton').click(); cy.contains(value).click(); }); Cypress.Commands.add('visitKibana', (url: string) => { cy.visit(url); - cy.get('[data-test-subj="kbnLoadingMessage"]').should('exist'); - cy.get('[data-test-subj="kbnLoadingMessage"]').should('not.exist', { + cy.getByTestSubj('kbnLoadingMessage').should('exist'); + cy.getByTestSubj('kbnLoadingMessage').should('not.exist', { timeout: 50000, }); }); @@ -70,13 +74,13 @@ Cypress.Commands.add( (start: string, end: string) => { const format = 'MMM D, YYYY @ HH:mm:ss.SSS'; - cy.get('[data-test-subj="superDatePickerstartDatePopoverButton"]').click(); - cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]') + cy.getByTestSubj('superDatePickerstartDatePopoverButton').click(); + cy.getByTestSubj('superDatePickerAbsoluteDateInput') .eq(0) .clear({ force: true }) .type(moment(start).format(format), { force: true }); - cy.get('[data-test-subj="superDatePickerendDatePopoverButton"]').click(); - cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]') + cy.getByTestSubj('superDatePickerendDatePopoverButton').click(); + cy.getByTestSubj('superDatePickerAbsoluteDateInput') .eq(1) .clear({ force: true }) .type(moment(end).format(format), { force: true }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index 2235847e584a..5d59d4691820 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -22,5 +22,6 @@ declare namespace Cypress { value: string; }): void; updateAdvancedSettings(settings: Record): void; + getByTestSubj(selector: string): Chainable>; } } diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts index 09333e3773fe..fc37906299d1 100644 --- a/x-pack/plugins/cloud/common/constants.ts +++ b/x-pack/plugins/cloud/common/constants.ts @@ -6,7 +6,6 @@ */ export const ELASTIC_SUPPORT_LINK = 'https://cloud.elastic.co/support'; -export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user'; /** * This is the page for managing your snapshots on Cloud. diff --git a/x-pack/plugins/cloud/kibana.json b/x-pack/plugins/cloud/kibana.json index 51df5d20d81b..85434abc87ed 100644 --- a/x-pack/plugins/cloud/kibana.json +++ b/x-pack/plugins/cloud/kibana.json @@ -7,7 +7,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "cloud"], - "optionalPlugins": ["cloudExperiments", "usageCollection", "home", "security"], + "optionalPlugins": ["usageCollection"], "server": true, "ui": true } diff --git a/x-pack/plugins/cloud/public/index.ts b/x-pack/plugins/cloud/public/index.ts index d50798cb15cd..ee37f85dfb6a 100644 --- a/x-pack/plugins/cloud/public/index.ts +++ b/x-pack/plugins/cloud/public/index.ts @@ -13,5 +13,3 @@ export type { CloudSetup, CloudConfigType, CloudStart } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new CloudPlugin(initializerContext); } - -export { Chat } from './components'; diff --git a/x-pack/plugins/cloud/public/mocks.tsx b/x-pack/plugins/cloud/public/mocks.tsx index f31596f3930f..608e826657b7 100644 --- a/x-pack/plugins/cloud/public/mocks.tsx +++ b/x-pack/plugins/cloud/public/mocks.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { CloudStart } from '.'; -import { ServicesProvider } from './services'; function createSetupMock() { return { @@ -19,28 +18,22 @@ function createSetupMock() { deploymentUrl: 'deployment-url', profileUrl: 'profile-url', organizationUrl: 'organization-url', + registerCloudService: jest.fn(), }; } -const config = { - chat: { - enabled: true, - chatURL: 'chat-url', - user: { - id: 'user-id', - email: 'test-user@elastic.co', - jwt: 'identity-jwt', - }, - }, -}; - const getContextProvider: () => React.FC = () => ({ children }) => - {children}; + <>{children}; const createStartMock = (): jest.Mocked => ({ CloudContextProvider: jest.fn(getContextProvider()), + cloudId: 'mock-cloud-id', + isCloudEnabled: true, + deploymentUrl: 'deployment-url', + profileUrl: 'profile-url', + organizationUrl: 'organization-url', }); export const cloudMock = { diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 599dee5e707b..efb566761e22 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -5,308 +5,18 @@ * 2.0. */ -import { firstValueFrom } from 'rxjs'; -import { Sha256 } from '@kbn/crypto-browser'; -import { nextTick } from '@kbn/test-jest-helpers'; import { coreMock } from '@kbn/core/public/mocks'; -import { homePluginMock } from '@kbn/home-plugin/public/mocks'; -import { securityMock } from '@kbn/security-plugin/public/mocks'; -import { CloudPlugin, type CloudConfigType } from './plugin'; -import { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common'; -import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks'; +import { CloudPlugin } from './plugin'; const baseConfig = { base_url: 'https://cloud.elastic.co', deployment_url: '/abc123', profile_url: '/user/settings/', organization_url: '/account/', - full_story: { - enabled: false, - }, - chat: { - enabled: false, - }, }; describe('Cloud Plugin', () => { describe('#setup', () => { - describe('setupFullStory', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - const setupPlugin = async ({ config = {} }: { config?: Partial }) => { - const initContext = coreMock.createPluginInitializerContext({ - ...baseConfig, - id: 'cloudId', - ...config, - }); - - const plugin = new CloudPlugin(initContext); - - const coreSetup = coreMock.createSetup(); - - const setup = plugin.setup(coreSetup, {}); - - // Wait for FullStory dynamic import to resolve - await new Promise((r) => setImmediate(r)); - - return { initContext, plugin, setup, coreSetup }; - }; - - test('register the shipper FullStory with correct args when enabled and org_id are set', async () => { - const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - }); - - expect(coreSetup.analytics.registerShipper).toHaveBeenCalled(); - expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), { - fullStoryOrgId: 'foo', - scriptUrl: '/internal/cloud/100/fullstory.js', - namespace: 'FSKibana', - }); - }); - - it('does not call initializeFullStory when enabled=false', async () => { - const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: false, org_id: 'foo' } }, - }); - expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); - }); - - it('does not call initializeFullStory when org_id is undefined', async () => { - const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } }); - expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); - }); - }); - - describe('setupTelemetryContext', () => { - const username = '1234'; - const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex'); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - const setupPlugin = async ({ - config = {}, - securityEnabled = true, - currentUserProps = {}, - }: { - config?: Partial; - securityEnabled?: boolean; - currentUserProps?: Record | Error; - }) => { - const initContext = coreMock.createPluginInitializerContext({ - ...baseConfig, - ...config, - }); - - const plugin = new CloudPlugin(initContext); - - const coreSetup = coreMock.createSetup(); - const securitySetup = securityMock.createSetup(); - if (currentUserProps instanceof Error) { - securitySetup.authc.getCurrentUser.mockRejectedValue(currentUserProps); - } else { - securitySetup.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser(currentUserProps) - ); - } - - const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); - - return { initContext, plugin, setup, coreSetup }; - }; - - test('register the context provider for the cloud user with hashed user ID when security is available', async () => { - const { coreSetup } = await setupPlugin({ - config: { id: 'cloudId' }, - currentUserProps: { username }, - }); - - expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); - - const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - )!; - - await expect(firstValueFrom(context$)).resolves.toEqual({ - userId: '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041', - isElasticCloudUser: false, - }); - }); - - it('user hash includes cloud id', async () => { - const { coreSetup: coreSetup1 } = await setupPlugin({ - config: { id: 'esOrg1' }, - currentUserProps: { username }, - }); - - const [{ context$: context1$ }] = - coreSetup1.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - )!; - - const { userId: hashId1 } = (await firstValueFrom(context1$)) as { userId: string }; - expect(hashId1).not.toEqual(expectedHashedPlainUsername); - - const { coreSetup: coreSetup2 } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' }, - currentUserProps: { username }, - }); - - const [{ context$: context2$ }] = - coreSetup2.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - )!; - - const { userId: hashId2 } = (await firstValueFrom(context2$)) as { userId: string }; - expect(hashId2).not.toEqual(expectedHashedPlainUsername); - - expect(hashId1).not.toEqual(hashId2); - }); - - test('user hash does not include cloudId when user is an Elastic Cloud user', async () => { - const { coreSetup } = await setupPlugin({ - config: { id: 'cloudDeploymentId' }, - currentUserProps: { username, elastic_cloud_user: true }, - }); - - expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); - - const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - )!; - - await expect(firstValueFrom(context$)).resolves.toEqual({ - userId: expectedHashedPlainUsername, - isElasticCloudUser: true, - }); - }); - - test('user hash does not include cloudId when not provided', async () => { - const { coreSetup } = await setupPlugin({ - config: {}, - currentUserProps: { username }, - }); - - expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); - - const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - )!; - - await expect(firstValueFrom(context$)).resolves.toEqual({ - userId: expectedHashedPlainUsername, - isElasticCloudUser: false, - }); - }); - - test('user hash is undefined when failed to fetch a user', async () => { - const { coreSetup } = await setupPlugin({ - currentUserProps: new Error('failed to fetch a user'), - }); - - expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); - - const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - )!; - - await expect(firstValueFrom(context$)).resolves.toEqual({ - userId: undefined, - isElasticCloudUser: false, - }); - }); - }); - - describe('setupChat', () => { - let consoleMock: jest.SpyInstance; - - beforeEach(() => { - consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleMock.mockRestore(); - }); - - const setupPlugin = async ({ - config = {}, - securityEnabled = true, - currentUserProps = {}, - isCloudEnabled = true, - failHttp = false, - }: { - config?: Partial; - securityEnabled?: boolean; - currentUserProps?: Record; - isCloudEnabled?: boolean; - failHttp?: boolean; - }) => { - const initContext = coreMock.createPluginInitializerContext({ - ...baseConfig, - id: isCloudEnabled ? 'cloud-id' : null, - ...config, - }); - - const plugin = new CloudPlugin(initContext); - - const coreSetup = coreMock.createSetup(); - const coreStart = coreMock.createStart(); - - if (failHttp) { - coreSetup.http.get.mockImplementation(() => { - throw new Error('HTTP request failed'); - }); - } - - coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]); - - const securitySetup = securityMock.createSetup(); - securitySetup.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser(currentUserProps) - ); - - const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); - - return { initContext, plugin, setup, coreSetup }; - }; - - it('chatConfig is not retrieved if cloud is not enabled', async () => { - const { coreSetup } = await setupPlugin({ isCloudEnabled: false }); - expect(coreSetup.http.get).not.toHaveBeenCalled(); - }); - - it('chatConfig is not retrieved if security is not enabled', async () => { - const { coreSetup } = await setupPlugin({ securityEnabled: false }); - expect(coreSetup.http.get).not.toHaveBeenCalled(); - }); - - it('chatConfig is not retrieved if chat is enabled but url is not provided', async () => { - // @ts-expect-error 2741 - const { coreSetup } = await setupPlugin({ config: { chat: { enabled: true } } }); - expect(coreSetup.http.get).not.toHaveBeenCalled(); - }); - - it('chatConfig is not retrieved if internal API fails', async () => { - const { coreSetup } = await setupPlugin({ - config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } }, - failHttp: true, - }); - expect(coreSetup.http.get).toHaveBeenCalled(); - expect(consoleMock).toHaveBeenCalled(); - }); - - it('chatConfig is retrieved if chat is enabled and url is provided', async () => { - const { coreSetup } = await setupPlugin({ - config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } }, - }); - expect(coreSetup.http.get).toHaveBeenCalled(); - }); - }); - describe('interface', () => { const setupPlugin = () => { const initContext = coreMock.createPluginInitializerContext({ @@ -317,7 +27,7 @@ describe('Cloud Plugin', () => { const plugin = new CloudPlugin(initContext); const coreSetup = coreMock.createSetup(); - const setup = plugin.setup(coreSetup, {}); + const setup = plugin.setup(coreSetup); return { setup }; }; @@ -361,49 +71,10 @@ describe('Cloud Plugin', () => { const { setup } = setupPlugin(); expect(setup.cname).toBe('cloud.elastic.co'); }); - }); - - describe('Set up cloudExperiments', () => { - describe('when cloud ID is not provided in the config', () => { - let cloudExperiments: jest.Mocked; - beforeEach(() => { - const plugin = new CloudPlugin(coreMock.createPluginInitializerContext(baseConfig)); - cloudExperiments = cloudExperimentsMock.createSetupMock(); - plugin.setup(coreMock.createSetup(), { cloudExperiments }); - }); - test('does not call cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser).not.toHaveBeenCalled(); - }); - }); - - describe('when cloud ID is provided in the config', () => { - let cloudExperiments: jest.Mocked; - beforeEach(() => { - const plugin = new CloudPlugin( - coreMock.createPluginInitializerContext({ ...baseConfig, id: 'cloud test' }) - ); - cloudExperiments = cloudExperimentsMock.createSetupMock(); - plugin.setup(coreMock.createSetup(), { cloudExperiments }); - }); - - test('calls cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser).toHaveBeenCalledTimes(1); - }); - - test('the cloud ID is hashed when calling cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser.mock.calls[0][0]).toEqual( - '1acb4a1cc1c3d672a8d826055d897c2623ceb1d4fb07e46d97986751a36b06cf' - ); - }); - - test('specifies the Kibana version when calling cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser.mock.calls[0][1]).toEqual( - expect.objectContaining({ - kibanaVersion: 'version', - }) - ); - }); + it('exposes registerCloudService', () => { + const { setup } = setupPlugin(); + expect(setup.registerCloudService).toBeDefined(); }); }); }); @@ -426,9 +97,8 @@ describe('Cloud Plugin', () => { }) ); const coreSetup = coreMock.createSetup(); - const homeSetup = homePluginMock.createSetupContract(); - plugin.setup(coreSetup, { home: homeSetup }); + plugin.setup(coreSetup); return { coreSetup, plugin }; }; @@ -437,8 +107,7 @@ describe('Cloud Plugin', () => { const { plugin } = startPlugin(); const coreStart = coreMock.createStart(); - const securityStart = securityMock.createStart(); - plugin.start(coreStart, { security: securityStart }); + plugin.start(coreStart); expect(coreStart.chrome.setHelpSupportUrl).toHaveBeenCalledTimes(1); expect(coreStart.chrome.setHelpSupportUrl.mock.calls[0]).toMatchInlineSnapshot(` @@ -447,177 +116,5 @@ describe('Cloud Plugin', () => { ] `); }); - - it('does not register custom nav links on anonymous pages', async () => { - const { plugin } = startPlugin(); - - const coreStart = coreMock.createStart(); - coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); - - const securityStart = securityMock.createStart(); - securityStart.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser({ - elastic_cloud_user: true, - }) - ); - - plugin.start(coreStart, { security: securityStart }); - - await nextTick(); - - expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled(); - expect(securityStart.authc.getCurrentUser).not.toHaveBeenCalled(); - }); - - it('registers a custom nav link for cloud users', async () => { - const { plugin } = startPlugin(); - - const coreStart = coreMock.createStart(); - const securityStart = securityMock.createStart(); - - securityStart.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser({ - elastic_cloud_user: true, - }) - ); - plugin.start(coreStart, { security: securityStart }); - - await nextTick(); - - expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1); - expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "euiIconType": "logoCloud", - "href": "https://cloud.elastic.co/abc123", - "title": "Manage this deployment", - }, - ] - `); - }); - - it('registers a custom nav link when there is an error retrieving the current user', async () => { - const { plugin } = startPlugin(); - - const coreStart = coreMock.createStart(); - const securityStart = securityMock.createStart(); - securityStart.authc.getCurrentUser.mockRejectedValue(new Error('something happened')); - plugin.start(coreStart, { security: securityStart }); - - await nextTick(); - - expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1); - expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "euiIconType": "logoCloud", - "href": "https://cloud.elastic.co/abc123", - "title": "Manage this deployment", - }, - ] - `); - }); - - it('does not register a custom nav link for non-cloud users', async () => { - const { plugin } = startPlugin(); - - const coreStart = coreMock.createStart(); - const securityStart = securityMock.createStart(); - securityStart.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser({ - elastic_cloud_user: false, - }) - ); - plugin.start(coreStart, { security: securityStart }); - - await nextTick(); - - expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled(); - }); - - it('registers user profile links for cloud users', async () => { - const { plugin } = startPlugin(); - - const coreStart = coreMock.createStart(); - const securityStart = securityMock.createStart(); - securityStart.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser({ - elastic_cloud_user: true, - }) - ); - plugin.start(coreStart, { security: securityStart }); - - await nextTick(); - - expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1); - expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "href": "https://cloud.elastic.co/profile/alice", - "iconType": "user", - "label": "Edit profile", - "order": 100, - "setAsProfile": true, - }, - Object { - "href": "https://cloud.elastic.co/org/myOrg", - "iconType": "gear", - "label": "Account & Billing", - "order": 200, - }, - ], - ] - `); - }); - - it('registers profile links when there is an error retrieving the current user', async () => { - const { plugin } = startPlugin(); - - const coreStart = coreMock.createStart(); - const securityStart = securityMock.createStart(); - securityStart.authc.getCurrentUser.mockRejectedValue(new Error('something happened')); - plugin.start(coreStart, { security: securityStart }); - - await nextTick(); - - expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1); - expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "href": "https://cloud.elastic.co/profile/alice", - "iconType": "user", - "label": "Edit profile", - "order": 100, - "setAsProfile": true, - }, - Object { - "href": "https://cloud.elastic.co/org/myOrg", - "iconType": "gear", - "label": "Account & Billing", - "order": 200, - }, - ], - ] - `); - }); - - it('does not register profile links for non-cloud users', async () => { - const { plugin } = startPlugin(); - - const coreStart = coreMock.createStart(); - const securityStart = securityMock.createStart(); - securityStart.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser({ - elastic_cloud_user: false, - }) - ); - plugin.start(coreStart, { security: securityStart }); - - await nextTick(); - - expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled(); - }); }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index c27668feb09b..f50f41f3c79c 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -6,34 +6,12 @@ */ import React, { FC } from 'react'; -import type { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, - HttpStart, - IBasePath, - AnalyticsServiceSetup, -} from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject, catchError, from, map, of } from 'rxjs'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; -import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; -import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; -import { Sha256 } from '@kbn/crypto-browser'; -import type { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common'; import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; -import { - ELASTIC_SUPPORT_LINK, - CLOUD_SNAPSHOTS_PATH, - GET_CHAT_USER_DATA_ROUTE_PATH, -} from '../common/constants'; -import type { GetChatUserDataResponseBody } from '../common/types'; -import { createUserMenuLinks } from './user_menu_links'; +import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants'; import { getFullCloudUrl } from './utils'; -import { ChatConfig, ServicesProvider } from './services'; export interface CloudConfigType { id?: string; @@ -47,23 +25,6 @@ export interface CloudConfigType { org_id?: string; eventTypesAllowlist?: string[]; }; - /** Configuration to enable live chat in Cloud-enabled instances of Kibana. */ - chat: { - /** Determines if chat is enabled. */ - enabled: boolean; - /** The URL to the remotely-hosted chat application. */ - chatURL: string; - }; -} - -interface CloudSetupDependencies { - home?: HomePublicPluginSetup; - security?: Pick; - cloudExperiments?: CloudExperimentsPluginSetup; -} - -interface CloudStartDependencies { - security?: SecurityPluginStart; } export interface CloudStart { @@ -71,6 +32,26 @@ export interface CloudStart { * A React component that provides a pre-wired `React.Context` which connects components to Cloud services. */ CloudContextProvider: FC<{}>; + /** + * `true` when Kibana is running on Elastic Cloud. + */ + isCloudEnabled: boolean; + /** + * Cloud ID. Undefined if not running on Cloud. + */ + cloudId?: string; + /** + * The full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud. + */ + deploymentUrl?: string; + /** + * The full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud. + */ + profileUrl?: string; + /** + * The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud. + */ + organizationUrl?: string; } export interface CloudSetup { @@ -82,268 +63,93 @@ export interface CloudSetup { organizationUrl?: string; snapshotsUrl?: string; isCloudEnabled: boolean; + registerCloudService: (contextProvider: FC) => void; } -interface SetupFullStoryDeps { - analytics: AnalyticsServiceSetup; - basePath: IBasePath; -} - -interface SetupChatDeps extends Pick { - http: CoreSetup['http']; +interface CloudUrls { + deploymentUrl?: string; + profileUrl?: string; + organizationUrl?: string; + snapshotsUrl?: string; } export class CloudPlugin implements Plugin { private readonly config: CloudConfigType; private readonly isCloudEnabled: boolean; - private chatConfig$ = new BehaviorSubject({ enabled: false }); + private readonly contextProviders: FC[] = []; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.isCloudEnabled = getIsCloudEnabled(this.config.id); } - public setup(core: CoreSetup, { cloudExperiments, home, security }: CloudSetupDependencies) { - this.setupTelemetryContext(core.analytics, security, this.config.id); - - this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) => - // eslint-disable-next-line no-console - console.debug(`Error setting up FullStory: ${e.toString()}`) - ); + public setup(core: CoreSetup): CloudSetup { + registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id); - const { - id, - cname, - profile_url: profileUrl, - organization_url: organizationUrl, - deployment_url: deploymentUrl, - base_url: baseUrl, - } = this.config; - - if (this.isCloudEnabled && id) { - // We use the Hashed Cloud Deployment ID as the userId in the Cloud Experiments - cloudExperiments?.identifyUser(sha256(id), { - kibanaVersion: this.initializerContext.env.packageInfo.version, - }); - } - - this.setupChat({ http: core.http, security }).catch((e) => - // eslint-disable-next-line no-console - console.debug(`Error setting up Chat: ${e.toString()}`) - ); - - if (home) { - home.environment.update({ cloud: this.isCloudEnabled }); - if (this.isCloudEnabled) { - home.tutorials.setVariable('cloud', { id, baseUrl, profileUrl, deploymentUrl }); - } - } - - const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl); - const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl); - const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl); - const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`; + const { id, cname, base_url: baseUrl } = this.config; return { cloudId: id, cname, baseUrl, - deploymentUrl: fullCloudDeploymentUrl, - profileUrl: fullCloudProfileUrl, - organizationUrl: fullCloudOrganizationUrl, - snapshotsUrl: fullCloudSnapshotsUrl, + ...this.getCloudUrls(), isCloudEnabled: this.isCloudEnabled, + registerCloudService: (contextProvider) => { + this.contextProviders.push(contextProvider); + }, }; } - public start(coreStart: CoreStart, { security }: CloudStartDependencies): CloudStart { - const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config; + public start(coreStart: CoreStart): CloudStart { coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); - const setLinks = (authorized: boolean) => { - if (!authorized) return; - - if (baseUrl && deploymentUrl) { - coreStart.chrome.setCustomNavLink({ - title: i18n.translate('xpack.cloud.deploymentLinkLabel', { - defaultMessage: 'Manage this deployment', - }), - euiIconType: 'logoCloud', - href: getFullCloudUrl(baseUrl, deploymentUrl), - }); - } - - if (security && this.isCloudEnabled) { - const userMenuLinks = createUserMenuLinks(this.config); - security.navControlService.addUserMenuLinks(userMenuLinks); - } - }; - - this.checkIfAuthorizedForLinks({ http: coreStart.http, security }) - .then(setLinks) - // In the event of an unexpected error, fail *open*. - // Cloud admin console will always perform the actual authorization checks. - .catch(() => setLinks(true)); - - // There's a risk that the request for chat config will take too much time to complete, and the provider - // will maintain a stale value. To avoid this, we'll use an Observable. + // Nest all the registered context providers under the Cloud Services Provider. + // This way, plugins only need to require Cloud's context provider to have all the enriched Cloud services. const CloudContextProvider: FC = ({ children }) => { - const chatConfig = useObservable(this.chatConfig$, { enabled: false }); - return {children}; + return ( + <> + {this.contextProviders.reduce( + (acc, ContextProvider) => ( + {acc} + ), + children + )} + + ); }; + const { deploymentUrl, profileUrl, organizationUrl } = this.getCloudUrls(); + return { CloudContextProvider, + isCloudEnabled: this.isCloudEnabled, + cloudId: this.config.id, + deploymentUrl, + profileUrl, + organizationUrl, }; } public stop() {} - /** - * Determines if the current user should see links back to Cloud. - * This isn't a true authorization check, but rather a heuristic to - * see if the current user is *likely* a cloud deployment administrator. - * - * At this point, we do not have enough information to reliably make this determination, - * but we do know that all cloud deployment admins are superusers by default. - */ - private async checkIfAuthorizedForLinks({ - http, - security, - }: { - http: HttpStart; - security?: SecurityPluginStart; - }) { - if (http.anonymousPaths.isAnonymous(window.location.pathname)) { - return false; - } - // Security plugin is disabled - if (!security) return true; - - // Otherwise check if user is a cloud user. - // If user is not defined due to an unexpected error, then fail *open*. - // Cloud admin console will always perform the actual authorization checks. - const user = await security.authc.getCurrentUser().catch(() => null); - return user?.elastic_cloud_user ?? true; - } - - /** - * If the right config is provided, register the FullStory shipper to the analytics client. - * @param analytics Core's Analytics service's setup contract. - * @param basePath Core's http.basePath helper. - * @private - */ - private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) { - const { enabled, org_id: fullStoryOrgId, eventTypesAllowlist } = this.config.full_story; - if (!enabled || !fullStoryOrgId) { - return; // do not load any FullStory code in the browser if not enabled - } - - // Keep this import async so that we do not load any FullStory code into the browser when it is disabled. - const { FullStoryShipper } = await import('@kbn/analytics-shippers-fullstory'); - analytics.registerShipper(FullStoryShipper, { - eventTypesAllowlist, - fullStoryOrgId, - // Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN. - scriptUrl: basePath.prepend( - `/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js` - ), - namespace: 'FSKibana', - }); - } - - /** - * Set up the Analytics context providers. - * @param analytics Core's Analytics service. The Setup contract. - * @param security The security plugin. - * @param cloudId The Cloud Org ID. - * @private - */ - private setupTelemetryContext( - analytics: AnalyticsServiceSetup, - security?: Pick, - cloudId?: string - ) { - registerCloudDeploymentIdAnalyticsContext(analytics, cloudId); - - if (security) { - analytics.registerContextProvider({ - name: 'cloud_user_id', - context$: from(security.authc.getCurrentUser()).pipe( - map((user) => { - if (user.elastic_cloud_user) { - // If the user is managed by ESS, use the plain username as the user ID: - // The username is expected to be unique for these users, - // and it matches how users are identified in the Cloud UI, so it allows us to correlate them. - return { userId: user.username, isElasticCloudUser: true }; - } - - return { - // For the rest of the authentication providers, we want to add the cloud deployment ID to make it unique. - // Especially in the case of Elasticsearch-backed authentication, where users are commonly repeated - // across multiple deployments (i.e.: `elastic` superuser). - userId: cloudId ? `${cloudId}:${user.username}` : user.username, - isElasticCloudUser: false, - }; - }), - // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs - map(({ userId, isElasticCloudUser }) => ({ userId: sha256(userId), isElasticCloudUser })), - catchError(() => of({ userId: undefined, isElasticCloudUser: false })) - ), - schema: { - userId: { - type: 'keyword', - _meta: { description: 'The user id scoped as seen by Cloud (hashed)' }, - }, - isElasticCloudUser: { - type: 'boolean', - _meta: { - description: '`true` if the user is managed by ESS.', - }, - }, - }, - }); - } - } - - private async setupChat({ http, security }: SetupChatDeps) { - if (!this.isCloudEnabled) { - return; - } - - const { enabled, chatURL } = this.config.chat; - - if (!security || !enabled || !chatURL) { - return; - } - - try { - const { - email, - id, - token: jwt, - } = await http.get(GET_CHAT_USER_DATA_ROUTE_PATH); + private getCloudUrls(): CloudUrls { + const { + profile_url: profileUrl, + organization_url: organizationUrl, + deployment_url: deploymentUrl, + base_url: baseUrl, + } = this.config; - if (!email || !id || !jwt) { - return; - } + const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl); + const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl); + const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl); + const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`; - this.chatConfig$.next({ - enabled, - chatURL, - user: { - email, - id, - jwt, - }, - }); - } catch (e) { - // eslint-disable-next-line no-console - console.debug(`[cloud.chat] Could not retrieve chat config: ${e.res.status} ${e.message}`, e); - } + return { + deploymentUrl: fullCloudDeploymentUrl, + profileUrl: fullCloudProfileUrl, + organizationUrl: fullCloudOrganizationUrl, + snapshotsUrl: fullCloudSnapshotsUrl, + }; } } - -function sha256(str: string) { - return new Sha256().update(str, 'utf8').digest('hex'); -} diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index aebbc65e50f1..512542c75679 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -18,32 +18,11 @@ const apmConfigSchema = schema.object({ ), }); -const fullStoryConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), - org_id: schema.conditional( - schema.siblingRef('enabled'), - true, - schema.string({ minLength: 1 }), - schema.maybe(schema.string()) - ), - eventTypesAllowlist: schema.arrayOf(schema.string(), { - defaultValue: ['Loaded Kibana'], - }), -}); - -const chatConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), - chatURL: schema.maybe(schema.string()), -}); - const configSchema = schema.object({ apm: schema.maybe(apmConfigSchema), base_url: schema.maybe(schema.string()), - chat: chatConfigSchema, - chatIdentitySecret: schema.maybe(schema.string()), cname: schema.maybe(schema.string()), deployment_url: schema.maybe(schema.string()), - full_story: fullStoryConfigSchema, id: schema.maybe(schema.string()), organization_url: schema.maybe(schema.string()), profile_url: schema.maybe(schema.string()), @@ -54,10 +33,8 @@ export type CloudConfigType = TypeOf; export const config: PluginConfigDescriptor = { exposeToBrowser: { base_url: true, - chat: true, cname: true, deployment_url: true, - full_story: true, id: true, organization_url: true, profile_url: true, diff --git a/x-pack/plugins/cloud/server/mocks.ts b/x-pack/plugins/cloud/server/mocks.ts new file mode 100644 index 000000000000..557e64edf6cc --- /dev/null +++ b/x-pack/plugins/cloud/server/mocks.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CloudSetup } from '.'; + +function createSetupMock(): jest.Mocked { + return { + cloudId: 'mock-cloud-id', + instanceSizeMb: 1234, + deploymentId: 'deployment-id', + isCloudEnabled: true, + apm: { + url: undefined, + secretToken: undefined, + }, + }; +} + +export const cloudMock = { + createSetup: createSetupMock, +}; diff --git a/x-pack/plugins/cloud/server/plugin.test.ts b/x-pack/plugins/cloud/server/plugin.test.ts index 05109a4c5481..55be923e98cf 100644 --- a/x-pack/plugins/cloud/server/plugin.test.ts +++ b/x-pack/plugins/cloud/server/plugin.test.ts @@ -7,111 +7,54 @@ import { coreMock } from '@kbn/core/server/mocks'; import { CloudPlugin } from './plugin'; -import { config } from './config'; -import { securityMock } from '@kbn/security-plugin/server/mocks'; -import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks'; -import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks'; -import { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common'; + +const baseConfig = { + base_url: 'https://cloud.elastic.co', + deployment_url: '/abc123', + profile_url: '/user/settings/', + organization_url: '/account/', +}; describe('Cloud Plugin', () => { describe('#setup', () => { - describe('setupSecurity', () => { - it('properly handles missing optional Security dependency if Cloud ID is NOT set.', async () => { - const plugin = new CloudPlugin( - coreMock.createPluginInitializerContext(config.schema.validate({})) - ); + describe('interface', () => { + const setupPlugin = () => { + const initContext = coreMock.createPluginInitializerContext({ + ...baseConfig, + id: 'cloudId', + cname: 'cloud.elastic.co', + }); + const plugin = new CloudPlugin(initContext); - expect(() => - plugin.setup(coreMock.createSetup(), { - usageCollection: usageCollectionPluginMock.createSetupContract(), - }) - ).not.toThrow(); - }); + const coreSetup = coreMock.createSetup(); + const setup = plugin.setup(coreSetup, {}); - it('properly handles missing optional Security dependency if Cloud ID is set.', async () => { - const plugin = new CloudPlugin( - coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' })) - ); + return { setup }; + }; - expect(() => - plugin.setup(coreMock.createSetup(), { - usageCollection: usageCollectionPluginMock.createSetupContract(), - }) - ).not.toThrow(); + it('exposes isCloudEnabled', () => { + const { setup } = setupPlugin(); + expect(setup.isCloudEnabled).toBe(true); }); - it('does not notify Security plugin about Cloud environment if Cloud ID is NOT set.', async () => { - const plugin = new CloudPlugin( - coreMock.createPluginInitializerContext(config.schema.validate({})) - ); - - const securityDependencyMock = securityMock.createSetup(); - plugin.setup(coreMock.createSetup(), { - security: securityDependencyMock, - usageCollection: usageCollectionPluginMock.createSetupContract(), - }); - - expect(securityDependencyMock.setIsElasticCloudDeployment).not.toHaveBeenCalled(); + it('exposes cloudId', () => { + const { setup } = setupPlugin(); + expect(setup.cloudId).toBe('cloudId'); }); - it('properly notifies Security plugin about Cloud environment if Cloud ID is set.', async () => { - const plugin = new CloudPlugin( - coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' })) - ); - - const securityDependencyMock = securityMock.createSetup(); - plugin.setup(coreMock.createSetup(), { - security: securityDependencyMock, - usageCollection: usageCollectionPluginMock.createSetupContract(), - }); - - expect(securityDependencyMock.setIsElasticCloudDeployment).toHaveBeenCalledTimes(1); + it('exposes instanceSizeMb', () => { + const { setup } = setupPlugin(); + expect(setup.instanceSizeMb).toBeUndefined(); }); - }); - describe('Set up cloudExperiments', () => { - describe('when cloud ID is not provided in the config', () => { - let cloudExperiments: jest.Mocked; - beforeEach(() => { - const plugin = new CloudPlugin( - coreMock.createPluginInitializerContext(config.schema.validate({})) - ); - cloudExperiments = cloudExperimentsMock.createSetupMock(); - plugin.setup(coreMock.createSetup(), { cloudExperiments }); - }); - - test('does not call cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser).not.toHaveBeenCalled(); - }); + it('exposes deploymentId', () => { + const { setup } = setupPlugin(); + expect(setup.deploymentId).toBe('abc123'); }); - describe('when cloud ID is provided in the config', () => { - let cloudExperiments: jest.Mocked; - beforeEach(() => { - const plugin = new CloudPlugin( - coreMock.createPluginInitializerContext(config.schema.validate({ id: 'cloud test' })) - ); - cloudExperiments = cloudExperimentsMock.createSetupMock(); - plugin.setup(coreMock.createSetup(), { cloudExperiments }); - }); - - test('calls cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser).toHaveBeenCalledTimes(1); - }); - - test('the cloud ID is hashed when calling cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser.mock.calls[0][0]).toEqual( - '1acb4a1cc1c3d672a8d826055d897c2623ceb1d4fb07e46d97986751a36b06cf' - ); - }); - - test('specifies the Kibana version when calling cloudExperiments.identifyUser', async () => { - expect(cloudExperiments.identifyUser.mock.calls[0][1]).toEqual( - expect.objectContaining({ - kibanaVersion: 'version', - }) - ); - }); + it('exposes apm', () => { + const { setup } = setupPlugin(); + expect(setup.apm).toStrictEqual({ url: undefined, secretToken: undefined }); }); }); }); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index d38a57a4d3ba..9cf1a308800a 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -5,24 +5,17 @@ * 2.0. */ -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; -import { CoreSetup, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; -import type { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common'; -import { createSHA256Hash } from '@kbn/crypto'; +import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; -import { CloudConfigType } from './config'; +import type { CloudConfigType } from './config'; import { registerCloudUsageCollector } from './collectors'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { parseDeploymentIdFromDeploymentUrl } from './utils'; -import { registerFullstoryRoute } from './routes/fullstory'; -import { registerChatRoute } from './routes/chat'; import { readInstanceSizeMb } from './env'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; - security?: SecurityPluginSetup; - cloudExperiments?: CloudExperimentsPluginSetup; } export interface CloudSetup { @@ -37,52 +30,17 @@ export interface CloudSetup { } export class CloudPlugin implements Plugin { - private readonly logger: Logger; private readonly config: CloudConfigType; - private readonly isDev: boolean; constructor(private readonly context: PluginInitializerContext) { - this.logger = this.context.logger.get(); this.config = this.context.config.get(); - this.isDev = this.context.env.mode.dev; } - public setup( - core: CoreSetup, - { cloudExperiments, usageCollection, security }: PluginsSetup - ): CloudSetup { - this.logger.debug('Setting up Cloud plugin'); + public setup(core: CoreSetup, { usageCollection }: PluginsSetup): CloudSetup { const isCloudEnabled = getIsCloudEnabled(this.config.id); registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); - if (isCloudEnabled) { - security?.setIsElasticCloudDeployment(); - } - - if (isCloudEnabled && this.config.id) { - // We use the Cloud ID as the userId in the Cloud Experiments - cloudExperiments?.identifyUser(createSHA256Hash(this.config.id), { - kibanaVersion: this.context.env.packageInfo.version, - }); - } - - if (this.config.full_story.enabled) { - registerFullstoryRoute({ - httpResources: core.http.resources, - packageInfo: this.context.env.packageInfo, - }); - } - - if (this.config.chat.enabled && this.config.chatIdentitySecret) { - registerChatRoute({ - router: core.http.createRouter(), - chatIdentitySecret: this.config.chatIdentitySecret, - security, - isDev: this.isDev, - }); - } - return { cloudId: this.config.id, instanceSizeMb: readInstanceSizeMb(), diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json index d8c8a5c8eca4..ca9ba32ed10b 100644 --- a/x-pack/plugins/cloud/tsconfig.json +++ b/x-pack/plugins/cloud/tsconfig.json @@ -16,8 +16,5 @@ "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/home/tsconfig.json" }, - { "path": "../cloud_integrations/cloud_experiments/tsconfig.json" }, - { "path": "../security/tsconfig.json" }, ] } diff --git a/x-pack/plugins/cloud/.storybook/decorator.tsx b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/decorator.tsx similarity index 89% rename from x-pack/plugins/cloud/.storybook/decorator.tsx rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/decorator.tsx index 4489b58f7575..3af8d04a598e 100644 --- a/x-pack/plugins/cloud/.storybook/decorator.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/decorator.tsx @@ -7,12 +7,11 @@ import React from 'react'; import { DecoratorFn } from '@storybook/react'; -import { ServicesProvider, CloudServices } from '../public/services'; +import { ServicesProvider, CloudChatServices } from '../public/services'; // TODO: move to a storybook implementation of the service using parameters. -const services: CloudServices = { +const services: CloudChatServices = { chat: { - enabled: true, chatURL: 'https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html', user: { id: 'user-id', diff --git a/x-pack/plugins/cloud/.storybook/index.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/index.ts similarity index 100% rename from x-pack/plugins/cloud/.storybook/index.ts rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/index.ts diff --git a/x-pack/plugins/cloud/.storybook/main.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/main.ts similarity index 100% rename from x-pack/plugins/cloud/.storybook/main.ts rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/main.ts diff --git a/x-pack/plugins/cloud/.storybook/manager.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/manager.ts similarity index 100% rename from x-pack/plugins/cloud/.storybook/manager.ts rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/manager.ts diff --git a/x-pack/plugins/cloud/.storybook/preview.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/preview.ts similarity index 100% rename from x-pack/plugins/cloud/.storybook/preview.ts rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/preview.ts diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/README.md b/x-pack/plugins/cloud_integrations/cloud_chat/README.md new file mode 100755 index 000000000000..cee3d9f5a667 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_chat/README.md @@ -0,0 +1,3 @@ +# Cloud Chat + +Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud. diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts b/x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts new file mode 100755 index 000000000000..d7bd133e5b4f --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user'; diff --git a/x-pack/plugins/cloud/common/types.ts b/x-pack/plugins/cloud_integrations/cloud_chat/common/types.ts similarity index 100% rename from x-pack/plugins/cloud/common/types.ts rename to x-pack/plugins/cloud_integrations/cloud_chat/common/types.ts diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js b/x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js new file mode 100644 index 000000000000..44f6f241d44d --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js @@ -0,0 +1,18 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../', + roots: ['/x-pack/plugins/cloud_integrations/cloud_chat'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_chat', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/cloud_integrations/cloud_chat/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/kibana.json b/x-pack/plugins/cloud_integrations/cloud_chat/kibana.json new file mode 100755 index 000000000000..76f7e34e71e5 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_chat/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "cloudChat", + "version": "1.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Core", + "githubTeam": "kibana-core" + }, + "description": "Chat available on Elastic Cloud deployments for quicker assistance.", + "server": true, + "ui": true, + "configPath": ["xpack", "cloud_integrations", "chat"], + "requiredPlugins": ["cloud"], + "optionalPlugins": ["security"] +} diff --git a/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx b/x-pack/plugins/cloud_integrations/cloud_chat/public/components/chat/chat.stories.tsx similarity index 99% rename from x-pack/plugins/cloud/public/components/chat/chat.stories.tsx rename to x-pack/plugins/cloud_integrations/cloud_chat/public/components/chat/chat.stories.tsx index 7e673e341cec..295750ee4303 100644 --- a/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_chat/public/components/chat/chat.stories.tsx @@ -68,7 +68,6 @@ export const Component = ({ id, email, chatURL, jwt }: Params) => { return ( {}, onReady, onResize }: Props) => { }} size="xs" > - {i18n.translate('xpack.cloud.chat.hideChatButtonLabel', { + {i18n.translate('xpack.cloudChat.hideChatButtonLabel', { defaultMessage: 'Hide chat', })} @@ -80,7 +80,7 @@ export const Chat = ({ onHide = () => {}, onReady, onResize }: Props) => { {button}