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}
-
+
{i18n.translate(
'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.docsLink',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
index 084fb4244cb7..66ffbe45c177 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
@@ -14,6 +14,8 @@ import { i18n } from '@kbn/i18n';
import noMlModelsGraphicDark from '../../../../../../assets/images/no_ml_models_dark.svg';
import noMlModelsGraphicLight from '../../../../../../assets/images/no_ml_models_light.svg';
+import { docLinks } from '../../../../../shared/doc_links';
+
export const NoModelsPanel: React.FC = () => {
const { colorMode } = useEuiTheme();
@@ -43,8 +45,7 @@ export const NoModelsPanel: React.FC = () => {
>
}
footer={
- // TODO: insert correct docsLink here
-
+
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.crawlRequestsTable.emptyPrompt.docsLink',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
index 07be63b54f3b..9cab24190a2d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
@@ -87,7 +87,7 @@ export const SearchIndexPipelines: React.FC = () => {
+
{i18n.translate(
'xpack.enterpriseSearch.content.indices.pipelines.mlInferencePipelines.docLink',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx
index 1bb6bb3777d8..01997959ec41 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx
@@ -19,7 +19,7 @@ import {
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
-import { Chat } from '@kbn/cloud-plugin/public';
+import { Chat } from '@kbn/cloud-chat-plugin/public';
import { i18n } from '@kbn/i18n';
import {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
index 17ee2230b2fb..975e7981829f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
@@ -65,6 +65,7 @@ class DocLinks {
public crawlerGettingStarted: string;
public crawlerManaging: string;
public crawlerOverview: string;
+ public deployTrainedModels: string;
public documentLevelSecurity: string;
public elasticsearchCreateIndex: string;
public elasticsearchGettingStarted: string;
@@ -178,6 +179,7 @@ class DocLinks {
this.crawlerGettingStarted = '';
this.crawlerManaging = '';
this.crawlerOverview = '';
+ this.deployTrainedModels = '';
this.documentLevelSecurity = '';
this.elasticsearchCreateIndex = '';
this.elasticsearchGettingStarted = '';
@@ -293,6 +295,7 @@ class DocLinks {
this.crawlerGettingStarted = docLinks.links.enterpriseSearch.crawlerGettingStarted;
this.crawlerManaging = docLinks.links.enterpriseSearch.crawlerManaging;
this.crawlerOverview = docLinks.links.enterpriseSearch.crawlerOverview;
+ this.deployTrainedModels = docLinks.links.enterpriseSearch.deployTrainedModels;
this.documentLevelSecurity = docLinks.links.enterpriseSearch.documentLevelSecurity;
this.elasticsearchCreateIndex = docLinks.links.elasticsearch.createIndex;
this.elasticsearchGettingStarted = docLinks.links.elasticsearch.gettingStarted;
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
index 0aaf30ef126d..9663b216ec91 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
@@ -254,8 +254,8 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
connectorId: schema.string(),
}),
body: schema.object({
- name: schema.maybe(schema.string()),
- description: schema.maybe(schema.string()),
+ name: schema.string(),
+ description: schema.nullable(schema.string()),
}),
},
},
diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json
index ce288f8b4b97..10fcc3b8c0d5 100644
--- a/x-pack/plugins/enterprise_search/tsconfig.json
+++ b/x-pack/plugins/enterprise_search/tsconfig.json
@@ -21,6 +21,7 @@
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
{ "path": "../cloud/tsconfig.json" },
+ { "path": "../cloud_integrations/cloud_chat/tsconfig.json" },
{ "path": "../infra/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
diff --git a/x-pack/plugins/fleet/.storybook/context/cloud.ts b/x-pack/plugins/fleet/.storybook/context/cloud.ts
index eccb41d6aa8c..1bd63d673bb3 100644
--- a/x-pack/plugins/fleet/.storybook/context/cloud.ts
+++ b/x-pack/plugins/fleet/.storybook/context/cloud.ts
@@ -17,6 +17,7 @@ export const getCloud = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => {
organizationUrl: 'https://organization.url',
profileUrl: 'https://profile.url',
snapshotsUrl: 'https://snapshots.url',
+ registerCloudService: () => {},
};
return cloud;
diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts
index fa54f8c943e2..ea2ad78dc10c 100644
--- a/x-pack/plugins/fleet/common/types/models/agent.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent.ts
@@ -94,7 +94,12 @@ interface AgentBase {
export interface Agent extends AgentBase {
id: string;
access_api_key?: string;
+ // @deprecated
default_api_key_history?: FleetServerAgent['default_api_key_history'];
+ outputs?: Array<{
+ api_key_id: string;
+ to_retire_api_key_ids?: FleetServerAgent['default_api_key_history'];
+ }>;
status?: AgentStatus;
packages: string[];
sort?: Array;
diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json
index 6ab87283e0b2..79d8bbd40644 100644
--- a/x-pack/plugins/fleet/kibana.json
+++ b/x-pack/plugins/fleet/kibana.json
@@ -11,5 +11,5 @@
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager"],
"optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry", "discover", "ingestPipelines"],
"extraPublicDirs": ["common"],
- "requiredBundles": ["kibanaReact", "cloud", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"]
+ "requiredBundles": ["kibanaReact", "cloudChat", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"]
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx
new file mode 100644
index 000000000000..55e91154060b
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { ExperimentalFeaturesService } from '../../../../../services';
+import {
+ generateCreatePackagePolicyDevToolsRequest,
+ generateCreateAgentPolicyDevToolsRequest,
+} from '../../../services';
+import {
+ FLEET_SYSTEM_PACKAGE,
+ HIDDEN_API_REFERENCE_PACKAGES,
+} from '../../../../../../../../common/constants';
+import type { PackageInfo, NewAgentPolicy, NewPackagePolicy } from '../../../../../types';
+import { SelectedPolicyTab } from '../../components';
+
+export function useDevToolsRequest({
+ newAgentPolicy,
+ packagePolicy,
+ packageInfo,
+ selectedPolicyTab,
+ withSysMonitoring,
+}: {
+ withSysMonitoring: boolean;
+ selectedPolicyTab: SelectedPolicyTab;
+ newAgentPolicy: NewAgentPolicy;
+ packagePolicy: NewPackagePolicy;
+ packageInfo?: PackageInfo;
+}) {
+ const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } =
+ ExperimentalFeaturesService.get();
+
+ const showDevtoolsRequest =
+ !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? '') &&
+ isShowDevtoolRequestExperimentEnabled;
+
+ const [devtoolRequest, devtoolRequestDescription] = useMemo(() => {
+ if (selectedPolicyTab === SelectedPolicyTab.NEW) {
+ const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
+ return [
+ `${generateCreateAgentPolicyDevToolsRequest(
+ newAgentPolicy,
+ withSysMonitoring && !packagePolicyIsSystem
+ )}\n\n${generateCreatePackagePolicyDevToolsRequest({
+ ...packagePolicy,
+ })}`,
+ i18n.translate(
+ 'xpack.fleet.createPackagePolicy.devtoolsRequestWithAgentPolicyDescription',
+ {
+ defaultMessage:
+ 'These Kibana requests creates a new agent policy and a new package policy.',
+ }
+ ),
+ ];
+ }
+
+ return [
+ generateCreatePackagePolicyDevToolsRequest({
+ ...packagePolicy,
+ }),
+ i18n.translate('xpack.fleet.createPackagePolicy.devtoolsRequestDescription', {
+ defaultMessage: 'This Kibana request creates a new package policy.',
+ }),
+ ];
+ }, [packagePolicy, newAgentPolicy, withSysMonitoring, selectedPolicyTab]);
+
+ return { showDevtoolsRequest, devtoolRequest, devtoolRequestDescription };
+}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
new file mode 100644
index 000000000000..e0f206ef612a
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
@@ -0,0 +1,318 @@
+/*
+ * 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 { useCallback, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { safeLoad } from 'js-yaml';
+
+import type {
+ AgentPolicy,
+ NewPackagePolicy,
+ NewAgentPolicy,
+ CreatePackagePolicyRequest,
+ PackagePolicy,
+ PackageInfo,
+} from '../../../../../types';
+import {
+ useStartServices,
+ sendCreateAgentPolicy,
+ sendCreatePackagePolicy,
+ sendBulkInstallPackages,
+} from '../../../../../hooks';
+import { isVerificationError } from '../../../../../services';
+import { FLEET_ELASTIC_AGENT_PACKAGE, FLEET_SYSTEM_PACKAGE } from '../../../../../../../../common';
+import { useConfirmForceInstall } from '../../../../../../integrations/hooks';
+import { validatePackagePolicy, validationHasErrors } from '../../services';
+import type { PackagePolicyValidationResults } from '../../services';
+import type { PackagePolicyFormState } from '../../types';
+import { SelectedPolicyTab } from '../../components';
+import { useOnSaveNavigate } from '../../hooks';
+
+async function createAgentPolicy({
+ packagePolicy,
+ newAgentPolicy,
+ withSysMonitoring,
+}: {
+ packagePolicy: NewPackagePolicy;
+ newAgentPolicy: NewAgentPolicy;
+ withSysMonitoring: boolean;
+}): Promise {
+ // do not create agent policy with system integration if package policy already is for system package
+ const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
+ const resp = await sendCreateAgentPolicy(newAgentPolicy, {
+ withSysMonitoring: withSysMonitoring && !packagePolicyIsSystem,
+ });
+ if (resp.error) {
+ throw resp.error;
+ }
+ if (!resp.data) {
+ throw new Error('Invalid agent policy creation no data');
+ }
+ return resp.data.item;
+}
+
+async function savePackagePolicy(pkgPolicy: CreatePackagePolicyRequest['body']) {
+ const result = await sendCreatePackagePolicy(pkgPolicy);
+
+ return result;
+}
+
+export function useOnSubmit({
+ agentCount,
+ selectedPolicyTab,
+ newAgentPolicy,
+ withSysMonitoring,
+ queryParamsPolicyId,
+ packageInfo,
+}: {
+ packageInfo?: PackageInfo;
+ newAgentPolicy: NewAgentPolicy;
+ withSysMonitoring: boolean;
+ selectedPolicyTab: SelectedPolicyTab;
+ agentCount: number;
+ queryParamsPolicyId: string | undefined;
+}) {
+ const { notifications } = useStartServices();
+ const confirmForceInstall = useConfirmForceInstall();
+ // only used to store the resulting package policy once saved
+ const [savedPackagePolicy, setSavedPackagePolicy] = useState();
+ // Form state
+ const [formState, setFormState] = useState('VALID');
+
+ const [agentPolicy, setAgentPolicy] = useState();
+ // New package policy state
+ const [packagePolicy, setPackagePolicy] = useState({
+ name: '',
+ description: '',
+ namespace: 'default',
+ policy_id: '',
+ enabled: true,
+ inputs: [],
+ });
+
+ // Validation state
+ const [validationResults, setValidationResults] = useState();
+ const [hasAgentPolicyError, setHasAgentPolicyError] = useState(false);
+ const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
+
+ // Update agent policy method
+ const updateAgentPolicy = useCallback(
+ (updatedAgentPolicy: AgentPolicy | undefined) => {
+ if (updatedAgentPolicy) {
+ setAgentPolicy(updatedAgentPolicy);
+ if (packageInfo) {
+ setHasAgentPolicyError(false);
+ }
+ } else {
+ setHasAgentPolicyError(true);
+ setAgentPolicy(undefined);
+ }
+
+ // eslint-disable-next-line no-console
+ console.debug('Agent policy updated', updatedAgentPolicy);
+ },
+ [packageInfo, setAgentPolicy]
+ );
+ // Update package policy validation
+ const updatePackagePolicyValidation = useCallback(
+ (newPackagePolicy?: NewPackagePolicy) => {
+ if (packageInfo) {
+ const newValidationResult = validatePackagePolicy(
+ newPackagePolicy || packagePolicy,
+ packageInfo,
+ safeLoad
+ );
+ setValidationResults(newValidationResult);
+ // eslint-disable-next-line no-console
+ console.debug('Package policy validation results', newValidationResult);
+
+ return newValidationResult;
+ }
+ },
+ [packagePolicy, packageInfo]
+ );
+ // Update package policy method
+ const updatePackagePolicy = useCallback(
+ (updatedFields: Partial) => {
+ const newPackagePolicy = {
+ ...packagePolicy,
+ ...updatedFields,
+ };
+ setPackagePolicy(newPackagePolicy);
+
+ // eslint-disable-next-line no-console
+ console.debug('Package policy updated', newPackagePolicy);
+ const newValidationResults = updatePackagePolicyValidation(newPackagePolicy);
+ const hasPackage = newPackagePolicy.package;
+ const hasValidationErrors = newValidationResults
+ ? validationHasErrors(newValidationResults)
+ : false;
+ const hasAgentPolicy = newPackagePolicy.policy_id && newPackagePolicy.policy_id !== '';
+ if (
+ hasPackage &&
+ (hasAgentPolicy || selectedPolicyTab === SelectedPolicyTab.NEW) &&
+ !hasValidationErrors
+ ) {
+ setFormState('VALID');
+ } else {
+ setFormState('INVALID');
+ }
+ },
+ [packagePolicy, setFormState, updatePackagePolicyValidation, selectedPolicyTab]
+ );
+
+ const onSaveNavigate = useOnSaveNavigate({
+ packagePolicy,
+ queryParamsPolicyId,
+ });
+
+ const navigateAddAgent = (policy?: PackagePolicy) =>
+ onSaveNavigate(policy, ['openEnrollmentFlyout']);
+
+ const navigateAddAgentHelp = (policy?: PackagePolicy) =>
+ onSaveNavigate(policy, ['showAddAgentHelp']);
+
+ const onSubmit = useCallback(
+ async ({
+ force,
+ overrideCreatedAgentPolicy,
+ }: { overrideCreatedAgentPolicy?: AgentPolicy; force?: boolean } = {}) => {
+ if (formState === 'VALID' && hasErrors) {
+ setFormState('INVALID');
+ return;
+ }
+ if (agentCount !== 0 && formState !== 'CONFIRM') {
+ setFormState('CONFIRM');
+ return;
+ }
+ let createdPolicy = overrideCreatedAgentPolicy;
+ if (selectedPolicyTab === SelectedPolicyTab.NEW && !overrideCreatedAgentPolicy) {
+ try {
+ setFormState('LOADING');
+ if ((withSysMonitoring || newAgentPolicy.monitoring_enabled?.length) ?? 0 > 0) {
+ const packagesToPreinstall: string[] = [];
+ if (packageInfo) {
+ packagesToPreinstall.push(packageInfo.name);
+ }
+ if (withSysMonitoring) {
+ packagesToPreinstall.push(FLEET_SYSTEM_PACKAGE);
+ }
+ if (newAgentPolicy.monitoring_enabled?.length ?? 0 > 0) {
+ packagesToPreinstall.push(FLEET_ELASTIC_AGENT_PACKAGE);
+ }
+
+ if (packagesToPreinstall.length > 0) {
+ await sendBulkInstallPackages([...new Set(packagesToPreinstall)]);
+ }
+ }
+
+ createdPolicy = await createAgentPolicy({
+ newAgentPolicy,
+ packagePolicy,
+ withSysMonitoring,
+ });
+ setAgentPolicy(createdPolicy);
+ updatePackagePolicy({ policy_id: createdPolicy.id });
+ } catch (e) {
+ setFormState('VALID');
+ notifications.toasts.addError(e, {
+ title: i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', {
+ defaultMessage: 'Unable to create agent policy',
+ }),
+ });
+ return;
+ }
+ }
+
+ setFormState('LOADING');
+ // passing pkgPolicy with policy_id here as setPackagePolicy doesn't propagate immediately
+ const { error, data } = await savePackagePolicy({
+ ...packagePolicy,
+ policy_id: createdPolicy?.id ?? packagePolicy.policy_id,
+ force,
+ });
+ setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS');
+ if (!error) {
+ setSavedPackagePolicy(data!.item);
+
+ const hasAgentsAssigned = agentCount && agentPolicy;
+ if (!hasAgentsAssigned) {
+ setFormState('SUBMITTED_NO_AGENTS');
+ return;
+ }
+ onSaveNavigate(data!.item);
+
+ notifications.toasts.addSuccess({
+ title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', {
+ defaultMessage: `'{packagePolicyName}' integration added.`,
+ values: {
+ packagePolicyName: packagePolicy.name,
+ },
+ }),
+ text: hasAgentsAssigned
+ ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
+ values: {
+ agentPolicyName: agentPolicy!.name,
+ },
+ })
+ : undefined,
+ 'data-test-subj': 'packagePolicyCreateSuccessToast',
+ });
+ } else {
+ if (isVerificationError(error)) {
+ setFormState('VALID'); // don't show the add agent modal
+ const forceInstall = await confirmForceInstall(packagePolicy.package!);
+
+ if (forceInstall) {
+ // skip creating the agent policy because it will have already been successfully created
+ onSubmit({ overrideCreatedAgentPolicy: createdPolicy, force: true });
+ }
+ return;
+ }
+ notifications.toasts.addError(error, {
+ title: 'Error',
+ });
+ setFormState('VALID');
+ }
+ },
+ [
+ formState,
+ hasErrors,
+ agentCount,
+ selectedPolicyTab,
+ packagePolicy,
+ notifications.toasts,
+ agentPolicy,
+ onSaveNavigate,
+ confirmForceInstall,
+ newAgentPolicy,
+ updatePackagePolicy,
+ withSysMonitoring,
+ packageInfo,
+ ]
+ );
+
+ return {
+ agentPolicy,
+ updateAgentPolicy,
+ packagePolicy,
+ updatePackagePolicy,
+ savedPackagePolicy,
+ onSubmit,
+ formState,
+ setFormState,
+ hasErrors,
+ validationResults,
+ setValidationResults,
+ hasAgentPolicyError,
+ setHasAgentPolicyError,
+ // TODO check
+ navigateAddAgent,
+ navigateAddAgentHelp,
+ };
+}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/index.tsx
new file mode 100644
index 000000000000..33d1cee84159
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/index.tsx
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { useDevToolsRequest } from './devtools_request';
+export { useOnSubmit } from './form';
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
index fae3c84f2126..02f36e2cadcf 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
@@ -21,35 +21,16 @@ import {
EuiErrorBoundary,
} from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';
-import { safeLoad } from 'js-yaml';
-import { useCancelAddPackagePolicy, useOnSaveNavigate } from '../hooks';
-import type { CreatePackagePolicyRequest } from '../../../../../../../common/types';
+import { useCancelAddPackagePolicy } from '../hooks';
import { splitPkgKey } from '../../../../../../../common/services';
-import {
- dataTypes,
- FLEET_SYSTEM_PACKAGE,
- HIDDEN_API_REFERENCE_PACKAGES,
-} from '../../../../../../../common/constants';
-import { useConfirmForceInstall } from '../../../../../integrations/hooks';
-import type {
- AgentPolicy,
- NewAgentPolicy,
- NewPackagePolicy,
- PackagePolicy,
-} from '../../../../types';
-import {
- sendCreatePackagePolicy,
- useStartServices,
- useConfig,
- sendGetAgentStatus,
- useGetPackageInfoByKey,
- sendCreateAgentPolicy,
-} from '../../../../hooks';
+import { dataTypes } from '../../../../../../../common/constants';
+import type { NewAgentPolicy } from '../../../../types';
+import { useConfig, sendGetAgentStatus, useGetPackageInfoByKey } from '../../../../hooks';
import {
Loading,
- Error,
+ Error as ErrorComponent,
ExtensionWrapper,
DevtoolsRequestFlyoutButton,
} from '../../../../components';
@@ -57,34 +38,21 @@ import {
import { agentPolicyFormValidation, ConfirmDeployAgentPolicyModal } from '../../components';
import { useUIExtension } from '../../../../hooks';
import type { PackagePolicyEditExtensionComponentProps } from '../../../../types';
-import {
- pkgKeyFromPackageInfo,
- isVerificationError,
- ExperimentalFeaturesService,
-} from '../../../../services';
+import { pkgKeyFromPackageInfo } from '../../../../services';
-import type {
- PackagePolicyFormState,
- AddToPolicyParams,
- CreatePackagePolicyParams,
-} from '../types';
+import type { AddToPolicyParams, CreatePackagePolicyParams } from '../types';
import { IntegrationBreadcrumb } from '../components';
-import type { PackagePolicyValidationResults } from '../services';
-import { validatePackagePolicy, validationHasErrors } from '../services';
import {
StepConfigurePackagePolicy,
StepDefinePackagePolicy,
SelectedPolicyTab,
StepSelectHosts,
} from '../components';
-import {
- generateCreatePackagePolicyDevToolsRequest,
- generateCreateAgentPolicyDevToolsRequest,
-} from '../../services';
import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components';
+import { useDevToolsRequest, useOnSubmit } from './hooks';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
@@ -106,12 +74,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
from,
queryParamsPolicyId,
}) => {
- const { notifications } = useStartServices();
const {
agents: { enabled: isFleetEnabled },
} = useConfig();
const { params } = useRouteMatch();
- const [agentPolicy, setAgentPolicy] = useState();
const [newAgentPolicy, setNewAgentPolicy] = useState({
name: 'Agent policy 1',
@@ -123,64 +89,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
const [withSysMonitoring, setWithSysMonitoring] = useState(true);
const validation = agentPolicyFormValidation(newAgentPolicy);
- // only used to store the resulting package policy once saved
- const [savedPackagePolicy, setSavedPackagePolicy] = useState();
-
- // Retrieve agent count
- const agentPolicyId = agentPolicy?.id;
-
- const { cancelClickHandler, cancelUrl } = useCancelAddPackagePolicy({
- from,
- pkgkey: params.pkgkey,
- agentPolicyId,
- });
- useEffect(() => {
- const getAgentCount = async () => {
- const { data } = await sendGetAgentStatus({ policyId: agentPolicyId });
- if (data?.results.total !== undefined) {
- setAgentCount(data.results.total);
- }
- };
-
- if (isFleetEnabled && agentPolicyId) {
- getAgentCount();
- }
- }, [agentPolicyId, isFleetEnabled]);
- const [agentCount, setAgentCount] = useState(0);
-
const [selectedPolicyTab, setSelectedPolicyTab] = useState(
queryParamsPolicyId ? SelectedPolicyTab.EXISTING : SelectedPolicyTab.NEW
);
- // New package policy state
- const [packagePolicy, setPackagePolicy] = useState({
- name: '',
- description: '',
- namespace: 'default',
- policy_id: '',
- enabled: true,
- inputs: [],
- });
-
- const onSaveNavigate = useOnSaveNavigate({
- packagePolicy,
- queryParamsPolicyId,
- });
- const navigateAddAgent = (policy?: PackagePolicy) =>
- onSaveNavigate(policy, ['openEnrollmentFlyout']);
-
- const navigateAddAgentHelp = (policy?: PackagePolicy) =>
- onSaveNavigate(policy, ['showAddAgentHelp']);
-
- const confirmForceInstall = useConfirmForceInstall();
-
- // Validation state
- const [validationResults, setValidationResults] = useState();
- const [hasAgentPolicyError, setHasAgentPolicyError] = useState(false);
-
- // Form state
- const [formState, setFormState] = useState('VALID');
-
const { pkgName, pkgVersion } = splitPkgKey(params.pkgkey);
// Fetch package info
const {
@@ -194,43 +106,50 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
}
}, [packageInfoData]);
- // Update agent policy method
- const updateAgentPolicy = useCallback(
- (updatedAgentPolicy: AgentPolicy | undefined) => {
- if (updatedAgentPolicy) {
- setAgentPolicy(updatedAgentPolicy);
- if (packageInfo) {
+ const [agentCount, setAgentCount] = useState(0);
+
+ // Save package policy
+ const {
+ onSubmit,
+ updatePackagePolicy,
+ packagePolicy,
+ agentPolicy,
+ updateAgentPolicy,
+ savedPackagePolicy,
+ formState,
+ setFormState,
+ navigateAddAgent,
+ navigateAddAgentHelp,
+ setHasAgentPolicyError,
+ validationResults,
+ hasAgentPolicyError,
+ } = useOnSubmit({
+ agentCount,
+ packageInfo,
+ newAgentPolicy,
+ selectedPolicyTab,
+ withSysMonitoring,
+ queryParamsPolicyId,
+ });
+
+ const setPolicyValidation = useCallback(
+ (selectedTab: SelectedPolicyTab, updatedAgentPolicy: NewAgentPolicy) => {
+ if (selectedTab === SelectedPolicyTab.NEW) {
+ if (
+ !updatedAgentPolicy.name ||
+ updatedAgentPolicy.name.trim() === '' ||
+ !updatedAgentPolicy.namespace ||
+ updatedAgentPolicy.namespace.trim() === ''
+ ) {
+ setHasAgentPolicyError(true);
+ } else {
setHasAgentPolicyError(false);
}
- } else {
- setHasAgentPolicyError(true);
- setAgentPolicy(undefined);
}
-
- // eslint-disable-next-line no-console
- console.debug('Agent policy updated', updatedAgentPolicy);
},
- [packageInfo, setAgentPolicy]
+ [setHasAgentPolicyError]
);
- const setPolicyValidation = (
- selectedTab: SelectedPolicyTab,
- updatedAgentPolicy: NewAgentPolicy
- ) => {
- if (selectedTab === SelectedPolicyTab.NEW) {
- if (
- !updatedAgentPolicy.name ||
- updatedAgentPolicy.name.trim() === '' ||
- !updatedAgentPolicy.namespace ||
- updatedAgentPolicy.namespace.trim() === ''
- ) {
- setHasAgentPolicyError(true);
- } else {
- setHasAgentPolicyError(false);
- }
- }
- };
-
const updateNewAgentPolicy = useCallback(
(updatedFields: Partial) => {
const updatedAgentPolicy = {
@@ -240,7 +159,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
setNewAgentPolicy(updatedAgentPolicy);
setPolicyValidation(selectedPolicyTab, updatedAgentPolicy);
},
- [setNewAgentPolicy, newAgentPolicy, selectedPolicyTab]
+ [setNewAgentPolicy, setPolicyValidation, newAgentPolicy, selectedPolicyTab]
);
const updateSelectedPolicyTab = useCallback(
@@ -248,58 +167,29 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
setSelectedPolicyTab(selectedTab);
setPolicyValidation(selectedTab, newAgentPolicy);
},
- [setSelectedPolicyTab, newAgentPolicy]
+ [setSelectedPolicyTab, setPolicyValidation, newAgentPolicy]
);
- const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
- // Update package policy validation
- const updatePackagePolicyValidation = useCallback(
- (newPackagePolicy?: NewPackagePolicy) => {
- if (packageInfo) {
- const newValidationResult = validatePackagePolicy(
- newPackagePolicy || packagePolicy,
- packageInfo,
- safeLoad
- );
- setValidationResults(newValidationResult);
- // eslint-disable-next-line no-console
- console.debug('Package policy validation results', newValidationResult);
-
- return newValidationResult;
- }
- },
- [packagePolicy, packageInfo]
- );
+ // Retrieve agent count
+ const agentPolicyId = agentPolicy?.id;
- // Update package policy method
- const updatePackagePolicy = useCallback(
- (updatedFields: Partial) => {
- const newPackagePolicy = {
- ...packagePolicy,
- ...updatedFields,
- };
- setPackagePolicy(newPackagePolicy);
-
- // eslint-disable-next-line no-console
- console.debug('Package policy updated', newPackagePolicy);
- const newValidationResults = updatePackagePolicyValidation(newPackagePolicy);
- const hasPackage = newPackagePolicy.package;
- const hasValidationErrors = newValidationResults
- ? validationHasErrors(newValidationResults)
- : false;
- const hasAgentPolicy = newPackagePolicy.policy_id && newPackagePolicy.policy_id !== '';
- if (
- hasPackage &&
- (hasAgentPolicy || selectedPolicyTab === SelectedPolicyTab.NEW) &&
- !hasValidationErrors
- ) {
- setFormState('VALID');
- } else {
- setFormState('INVALID');
+ const { cancelClickHandler, cancelUrl } = useCancelAddPackagePolicy({
+ from,
+ pkgkey: params.pkgkey,
+ agentPolicyId,
+ });
+ useEffect(() => {
+ const getAgentCount = async () => {
+ const { data } = await sendGetAgentStatus({ policyId: agentPolicyId });
+ if (data?.results.total !== undefined) {
+ setAgentCount(data.results.total);
}
- },
- [packagePolicy, updatePackagePolicyValidation, selectedPolicyTab]
- );
+ };
+
+ if (isFleetEnabled && agentPolicyId) {
+ getAgentCount();
+ }
+ }, [agentPolicyId, isFleetEnabled]);
const handleExtensionViewOnChange = useCallback<
PackagePolicyEditExtensionComponentProps['onChange']
@@ -313,132 +203,16 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
return prevState;
});
},
- [updatePackagePolicy]
- );
-
- // Save package policy
- const savePackagePolicy = useCallback(
- async (pkgPolicy: CreatePackagePolicyRequest['body']) => {
- setFormState('LOADING');
- const result = await sendCreatePackagePolicy(pkgPolicy);
- setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS');
- return result;
- },
- [agentCount]
+ [updatePackagePolicy, setFormState]
);
- const createAgentPolicy = useCallback(async (): Promise => {
- let createdAgentPolicy;
- setFormState('LOADING');
- // do not create agent policy with system integration if package policy already is for system package
- const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
- const resp = await sendCreateAgentPolicy(newAgentPolicy, {
- withSysMonitoring: withSysMonitoring && !packagePolicyIsSystem,
- });
- if (resp.error) {
- setFormState('VALID');
- throw resp.error;
- }
- if (resp.data) {
- createdAgentPolicy = resp.data.item;
- setAgentPolicy(createdAgentPolicy);
- updatePackagePolicy({ policy_id: createdAgentPolicy.id });
- }
- return createdAgentPolicy;
- }, [packagePolicy?.package?.name, newAgentPolicy, withSysMonitoring, updatePackagePolicy]);
-
- const onSubmit = useCallback(
- async ({
- force,
- overrideCreatedAgentPolicy,
- }: { overrideCreatedAgentPolicy?: AgentPolicy; force?: boolean } = {}) => {
- if (formState === 'VALID' && hasErrors) {
- setFormState('INVALID');
- return;
- }
- if (agentCount !== 0 && formState !== 'CONFIRM') {
- setFormState('CONFIRM');
- return;
- }
- let createdPolicy = overrideCreatedAgentPolicy;
- if (selectedPolicyTab === SelectedPolicyTab.NEW && !overrideCreatedAgentPolicy) {
- try {
- createdPolicy = await createAgentPolicy();
- } catch (e) {
- notifications.toasts.addError(e, {
- title: i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', {
- defaultMessage: 'Unable to create agent policy',
- }),
- });
- return;
- }
- }
-
- setFormState('LOADING');
- // passing pkgPolicy with policy_id here as setPackagePolicy doesn't propagate immediately
- const { error, data } = await savePackagePolicy({
- ...packagePolicy,
- policy_id: createdPolicy?.id ?? packagePolicy.policy_id,
- force,
- });
- if (!error) {
- setSavedPackagePolicy(data!.item);
-
- const hasAgentsAssigned = agentCount && agentPolicy;
- if (!hasAgentsAssigned) {
- setFormState('SUBMITTED_NO_AGENTS');
- return;
- }
- onSaveNavigate(data!.item);
-
- notifications.toasts.addSuccess({
- title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', {
- defaultMessage: `'{packagePolicyName}' integration added.`,
- values: {
- packagePolicyName: packagePolicy.name,
- },
- }),
- text: hasAgentsAssigned
- ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
- defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
- values: {
- agentPolicyName: agentPolicy!.name,
- },
- })
- : undefined,
- 'data-test-subj': 'packagePolicyCreateSuccessToast',
- });
- } else {
- if (isVerificationError(error)) {
- setFormState('VALID'); // don't show the add agent modal
- const forceInstall = await confirmForceInstall(packagePolicy.package!);
-
- if (forceInstall) {
- // skip creating the agent policy because it will have already been successfully created
- onSubmit({ overrideCreatedAgentPolicy: createdPolicy, force: true });
- }
- return;
- }
- notifications.toasts.addError(error, {
- title: 'Error',
- });
- setFormState('VALID');
- }
- },
- [
- formState,
- hasErrors,
- agentCount,
- selectedPolicyTab,
- savePackagePolicy,
- packagePolicy,
- createAgentPolicy,
- notifications.toasts,
- agentPolicy,
- onSaveNavigate,
- confirmForceInstall,
- ]
- );
+ const { devtoolRequest, devtoolRequestDescription, showDevtoolsRequest } = useDevToolsRequest({
+ newAgentPolicy,
+ packagePolicy,
+ selectedPolicyTab,
+ withSysMonitoring,
+ packageInfo,
+ });
const integrationInfo = useMemo(
() =>
@@ -488,6 +262,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
withSysMonitoring,
updateSelectedPolicyTab,
queryParamsPolicyId,
+ setHasAgentPolicyError,
]
);
@@ -564,47 +339,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
},
];
- const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } =
- ExperimentalFeaturesService.get();
-
- const showDevtoolsRequest =
- !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? '') &&
- isShowDevtoolRequestExperimentEnabled;
-
- const [devtoolRequest, devtoolRequestDescription] = useMemo(() => {
- if (selectedPolicyTab === SelectedPolicyTab.NEW) {
- const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
- return [
- `${generateCreateAgentPolicyDevToolsRequest(
- newAgentPolicy,
- withSysMonitoring && !packagePolicyIsSystem
- )}\n\n${generateCreatePackagePolicyDevToolsRequest({
- ...packagePolicy,
- })}`,
- i18n.translate(
- 'xpack.fleet.createPackagePolicy.devtoolsRequestWithAgentPolicyDescription',
- {
- defaultMessage:
- 'These Kibana requests creates a new agent policy and a new package policy.',
- }
- ),
- ];
- }
-
- return [
- generateCreatePackagePolicyDevToolsRequest({
- ...packagePolicy,
- }),
- i18n.translate('xpack.fleet.createPackagePolicy.devtoolsRequestDescription', {
- defaultMessage: 'This Kibana request creates a new package policy.',
- }),
- ];
- }, [packagePolicy, newAgentPolicy, withSysMonitoring, selectedPolicyTab]);
-
// Display package error if there is one
if (packageInfoError) {
return (
- {
+ return sendRequest({
+ path: epmRouteService.getBulkInstallPath(),
+ method: 'post',
+ body: {
+ packages,
+ },
+ });
+};
+
export const sendRemovePackage = (pkgName: string, pkgVersion: string, force: boolean = false) => {
return sendRequest({
path: epmRouteService.getRemovePath(pkgName, pkgVersion),
diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts
index 5eafcf1a9410..aefcbfc5cd87 100644
--- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts
+++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts
@@ -22,5 +22,19 @@ describe('upgrade handler', () => {
it('should not throw if upgrade version is equal to kibana version with snapshot', () => {
expect(() => checkKibanaVersion('8.4.0', '8.4.0-SNAPSHOT')).not.toThrowError();
});
+
+ it('should not throw if force is specified and patch is newer', () => {
+ expect(() => checkKibanaVersion('8.4.1', '8.4.0', true)).not.toThrowError();
+ expect(() => checkKibanaVersion('8.4.1-SNAPSHOT', '8.4.0', true)).not.toThrowError();
+ });
+
+ it('should throw if force is specified and minor is newer', () => {
+ expect(() => checkKibanaVersion('8.5.0', '8.4.0', true)).toThrowError();
+ });
+
+ it('should not throw if force is specified and major and minor is newer', () => {
+ expect(() => checkKibanaVersion('7.5.0', '8.4.0', true)).not.toThrowError();
+ expect(() => checkKibanaVersion('8.4.0', '8.4.0', true)).not.toThrowError();
+ });
});
});
diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts
index a79edbaa3685..d3fffac7d905 100644
--- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts
+++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts
@@ -10,6 +10,8 @@ import type { TypeOf } from '@kbn/config-schema';
import semverCoerce from 'semver/functions/coerce';
import semverGt from 'semver/functions/gt';
+import semverMajor from 'semver/functions/major';
+import semverMinor from 'semver/functions/minor';
import type { PostAgentUpgradeResponse, GetCurrentUpgradesResponse } from '../../../common/types';
import type { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types';
@@ -34,7 +36,7 @@ export const postAgentUpgradeHandler: RequestHandler<
const { version, source_uri: sourceUri, force } = request.body;
const kibanaVersion = appContextService.getKibanaVersion();
try {
- checkKibanaVersion(version, kibanaVersion);
+ checkKibanaVersion(version, kibanaVersion, force);
} catch (err) {
return response.customError({
statusCode: 400,
@@ -114,9 +116,9 @@ export const postBulkAgentsUpgradeHandler: RequestHandler<
} = request.body;
const kibanaVersion = appContextService.getKibanaVersion();
try {
- checkKibanaVersion(version, kibanaVersion);
+ checkKibanaVersion(version, kibanaVersion, force);
const fleetServerAgents = await getAllFleetServerAgents(soClient, esClient);
- checkFleetServerVersion(version, fleetServerAgents);
+ checkFleetServerVersion(version, fleetServerAgents, force);
} catch (err) {
return response.customError({
statusCode: 400,
@@ -158,7 +160,7 @@ export const getCurrentUpgradesHandler: RequestHandler = async (context, request
}
};
-export const checkKibanaVersion = (version: string, kibanaVersion: string) => {
+export const checkKibanaVersion = (version: string, kibanaVersion: string, force = false) => {
// get version number only in case "-SNAPSHOT" is in it
const kibanaVersionNumber = semverCoerce(kibanaVersion)?.version;
if (!kibanaVersionNumber) throw new Error(`kibanaVersion ${kibanaVersionNumber} is not valid`);
@@ -166,14 +168,31 @@ export const checkKibanaVersion = (version: string, kibanaVersion: string) => {
if (!versionToUpgradeNumber)
throw new Error(`version to upgrade ${versionToUpgradeNumber} is not valid`);
- if (semverGt(versionToUpgradeNumber, kibanaVersionNumber))
+ if (!force && semverGt(versionToUpgradeNumber, kibanaVersionNumber)) {
throw new Error(
`cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the installed kibana version ${kibanaVersionNumber}`
);
+ }
+
+ const kibanaMajorGt = semverMajor(kibanaVersionNumber) > semverMajor(versionToUpgradeNumber);
+ const kibanaMajorEqMinorGte =
+ semverMajor(kibanaVersionNumber) === semverMajor(versionToUpgradeNumber) &&
+ semverMinor(kibanaVersionNumber) >= semverMinor(versionToUpgradeNumber);
+
+ // When force is enabled, only the major and minor versions are checked
+ if (force && !(kibanaMajorGt || kibanaMajorEqMinorGte)) {
+ throw new Error(
+ `cannot force upgrade agent to ${versionToUpgradeNumber} because it does not satisfy the major and minor of the installed kibana version ${kibanaVersionNumber}`
+ );
+ }
};
// Check the installed fleet server version
-const checkFleetServerVersion = (versionToUpgradeNumber: string, fleetServerAgents: Agent[]) => {
+const checkFleetServerVersion = (
+ versionToUpgradeNumber: string,
+ fleetServerAgents: Agent[],
+ force = false
+) => {
const fleetServerVersions = fleetServerAgents.map(
(agent) => agent.local_metadata.elastic.agent.version
) as string[];
@@ -184,9 +203,22 @@ const checkFleetServerVersion = (versionToUpgradeNumber: string, fleetServerAgen
return;
}
- if (semverGt(versionToUpgradeNumber, maxFleetServerVersion)) {
+ if (!force && semverGt(versionToUpgradeNumber, maxFleetServerVersion)) {
throw new Error(
`cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the latest fleet server version ${maxFleetServerVersion}`
);
}
+
+ const fleetServerMajorGt =
+ semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
+ const fleetServerMajorEqMinorGte =
+ semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
+ semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);
+
+ // When force is enabled, only the major and minor versions are checked
+ if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
+ throw new Error(
+ `cannot force upgrade agent to ${versionToUpgradeNumber} because it does not satisfy the major and minor of the latest fleet server version ${maxFleetServerVersion}`
+ );
+ }
};
diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
index 5beb5c0a9ac0..9169df19fbcf 100644
--- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
+++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
@@ -331,6 +331,15 @@ describe('invalidateAPIKeysForAgents', () => {
id: 'defaultApiKeyHistory2',
},
],
+ outputs: [
+ {
+ api_key_id: 'outputApiKey1',
+ to_retire_api_key_ids: [{ id: 'outputApiKeyRetire1' }, { id: 'outputApiKeyRetire2' }],
+ },
+ {
+ api_key_id: 'outputApiKey2',
+ },
+ ],
} as any,
]);
@@ -340,6 +349,10 @@ describe('invalidateAPIKeysForAgents', () => {
'defaultApiKey1',
'defaultApiKeyHistory1',
'defaultApiKeyHistory2',
+ 'outputApiKey1',
+ 'outputApiKeyRetire1',
+ 'outputApiKeyRetire2',
+ 'outputApiKey2',
]);
});
});
diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts
index c735254f1825..fed5d44fe98e 100644
--- a/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts
+++ b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts
@@ -215,6 +215,16 @@ export async function invalidateAPIKeysForAgents(agents: Agent[]) {
if (agent.default_api_key_history) {
agent.default_api_key_history.forEach((apiKey) => keys.push(apiKey.id));
}
+ if (agent.outputs) {
+ agent.outputs.forEach((output) => {
+ if (output.api_key_id) {
+ keys.push(output.api_key_id);
+ }
+ if (output.to_retire_api_key_ids) {
+ output.to_retire_api_key_ids.forEach((apiKey) => keys.push(apiKey.id));
+ }
+ });
+ }
return keys;
}, []);
diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json
index 7cc16fe65426..320843546a30 100644
--- a/x-pack/plugins/fleet/tsconfig.json
+++ b/x-pack/plugins/fleet/tsconfig.json
@@ -36,6 +36,7 @@
{ "path": "../../../src/plugins/home/tsconfig.json" },
// requiredBundles from ./kibana.json
+ { "path": "../cloud_integrations/cloud_chat/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
{ "path": "../infra/tsconfig.json" },
diff --git a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts
index 820f8a4f9100..75db772ec092 100644
--- a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts
@@ -11,6 +11,7 @@ import {
CloudNodeAllocationTestBed,
setupCloudNodeAllocation,
} from './cloud_aware_behavior.helpers';
+import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
describe(' node allocation cloud-aware behavior', () => {
let testBed: CloudNodeAllocationTestBed;
@@ -28,7 +29,7 @@ describe(' node allocation cloud-aware behavior', () => {
await act(async () => {
if (Boolean(isOnCloud)) {
testBed = await setupCloudNodeAllocation(httpSetup, {
- appServicesContext: { cloud: { isCloudEnabled: true } },
+ appServicesContext: { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true } },
});
} else {
testBed = await setupCloudNodeAllocation(httpSetup);
diff --git a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts
index 68e74e23a781..fbe724c881af 100644
--- a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts
@@ -15,6 +15,7 @@ import {
SearchableSnapshotsTestBed,
setupSearchableSnapshotsTestBed,
} from './searchable_snapshots.helpers';
+import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
describe(' searchable snapshots', () => {
let testBed: SearchableSnapshotsTestBed;
@@ -142,7 +143,7 @@ describe(' searchable snapshots', () => {
await act(async () => {
testBed = await setupSearchableSnapshotsTestBed(httpSetup, {
- appServicesContext: { cloud: { isCloudEnabled: true } },
+ appServicesContext: { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true } },
});
});
@@ -171,7 +172,7 @@ describe(' searchable snapshots', () => {
await act(async () => {
testBed = await setupSearchableSnapshotsTestBed(httpSetup, {
- appServicesContext: { cloud: { isCloudEnabled: true } },
+ appServicesContext: { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true } },
});
});
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
index 74f2468eb4c4..e92ac801e861 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
@@ -8,8 +8,10 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { Query, TimeRange } from '@kbn/es-query';
-import React from 'react';
+import React, { useState } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
+import { i18n } from '@kbn/i18n';
+import { NoData } from '../../../../components/empty_states';
import { InfraClientStartDeps } from '../../../../types';
const getLensHostsTable = (
@@ -498,23 +500,54 @@ interface Props {
timeRange: TimeRange;
query: Query;
searchSessionId: string;
+ onRefetch: () => void;
+ onLoading: (isLoading: boolean) => void;
+ isLensLoading: boolean;
}
export const HostsTable: React.FunctionComponent = ({
dataView,
timeRange,
query,
searchSessionId,
+ onRefetch,
+ onLoading,
+ isLensLoading,
}) => {
const {
services: { lens },
} = useKibana();
const LensComponent = lens?.EmbeddableComponent;
+ const [noData, setNoData] = useState(false);
+
+ if (noData && !isLensLoading) {
+ return (
+
+ );
+ }
return (
{
+ if (!isLoading && adapters?.tables) {
+ setNoData(adapters?.tables.tables.default?.rows.length === 0);
+ onLoading(false);
+ }
+ }}
/>
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
index 63e95a19f1c7..7bf087db39eb 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
@@ -27,6 +27,7 @@ export const HostsContent: React.FunctionComponent = () => {
useMetricsDataViewContext();
// needed to refresh the lens table when filters havent changed
const [searchSessionId, setSearchSessionId] = useState(data.search.session.start());
+ const [isLensLoading, setIsLensLoading] = useState(false);
const onQuerySubmit = useCallback(
(payload: { dateRange: TimeRange; query?: Query }) => {
@@ -34,11 +35,26 @@ export const HostsContent: React.FunctionComponent = () => {
if (payload.query) {
setQuery(payload.query);
}
+ setIsLensLoading(true);
setSearchSessionId(data.search.session.start());
},
[setDateRange, setQuery, data.search.session]
);
+ const onLoading = useCallback(
+ (isLoading: boolean) => {
+ if (isLensLoading) {
+ setIsLensLoading(isLoading);
+ }
+ },
+ [setIsLensLoading, isLensLoading]
+ );
+
+ const onRefetch = useCallback(() => {
+ setIsLensLoading(true);
+ setSearchSessionId(data.search.session.start());
+ }, [data.search.session]);
+
return (
{metricsDataView ? (
@@ -61,6 +77,9 @@ export const HostsContent: React.FunctionComponent = () => {
timeRange={dateRange}
query={query}
searchSessionId={searchSessionId}
+ onRefetch={onRefetch}
+ onLoading={onLoading}
+ isLensLoading={isLensLoading}
/>
>
) : hasFailedCreatingDataView || hasFailedFetchingDataView ? (
diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts
index 6eb6663b10ee..c14a13d1a7ea 100644
--- a/x-pack/plugins/infra/public/plugin.ts
+++ b/x-pack/plugins/infra/public/plugin.ts
@@ -5,12 +5,17 @@
* 2.0.
*/
+import {
+ AppMountParameters,
+ AppUpdater,
+ CoreStart,
+ DEFAULT_APP_CATEGORIES,
+ PluginInitializerContext,
+} from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
-import { AppMountParameters, PluginInitializerContext } from '@kbn/core/public';
-import { from } from 'rxjs';
-import { map } from 'rxjs/operators';
-import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { enableInfrastructureHostsView } from '@kbn/observability-plugin/public';
+import { BehaviorSubject, combineLatest, from } from 'rxjs';
+import { map } from 'rxjs/operators';
import { defaultLogViewsStaticConfig } from '../common/log_views';
import { InfraPublicConfig } from '../common/plugin_config_types';
import { createInventoryMetricRuleType } from './alerting/inventory';
@@ -38,6 +43,7 @@ import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_
export class Plugin implements InfraClientPluginClass {
public config: InfraPublicConfig;
private logViews: LogViewsService;
+ private readonly appUpdater$ = new BehaviorSubject
(() => ({}));
constructor(context: PluginInitializerContext) {
this.config = context.config.get();
@@ -74,6 +80,11 @@ export class Plugin implements InfraClientPluginClass {
fetchData: createMetricsFetchData(core.getStartServices),
});
+ const startDep$AndHostViewFlag$ = combineLatest([
+ from(core.getStartServices()),
+ core.uiSettings.get$(enableInfrastructureHostsView),
+ ]);
+
/** !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts */
const infraEntries = [
{ label: 'Inventory', app: 'metrics', path: '/inventory' },
@@ -81,12 +92,15 @@ export class Plugin implements InfraClientPluginClass {
];
const hostInfraEntry = { label: 'Hosts', app: 'metrics', path: '/hosts' };
pluginsSetup.observability.navigation.registerSections(
- from(core.getStartServices()).pipe(
+ startDep$AndHostViewFlag$.pipe(
map(
([
- {
- application: { capabilities },
- },
+ [
+ {
+ application: { capabilities },
+ },
+ ],
+ isInfrastructureHostsViewEnabled,
]) => [
...(capabilities.logs.show
? [
@@ -106,7 +120,7 @@ export class Plugin implements InfraClientPluginClass {
{
label: 'Infrastructure',
sortKey: 300,
- entries: core.uiSettings.get(enableInfrastructureHostsView)
+ entries: isInfrastructureHostsViewEnabled
? [hostInfraEntry, ...infraEntries]
: infraEntries,
},
@@ -171,6 +185,7 @@ export class Plugin implements InfraClientPluginClass {
},
});
+ // !! Need to be kept in sync with the routes in x-pack/plugins/infra/public/pages/metrics/index.tsx
const infraDeepLinks = [
{
id: 'inventory',
@@ -210,8 +225,8 @@ export class Plugin implements InfraClientPluginClass {
order: 8200,
appRoute: '/app/metrics',
category: DEFAULT_APP_CATEGORIES.observability,
- // !! Need to be kept in sync with the routes in x-pack/plugins/infra/public/pages/metrics/index.tsx
- deepLinks: core.uiSettings.get(enableInfrastructureHostsView)
+ updater$: this.appUpdater$,
+ deepLinks: core.uiSettings.get(enableInfrastructureHostsView)
? [hostInfraDeepLink, ...infraDeepLinks]
: infraDeepLinks,
mount: async (params: AppMountParameters) => {
@@ -223,6 +238,19 @@ export class Plugin implements InfraClientPluginClass {
},
});
+ startDep$AndHostViewFlag$.subscribe(
+ ([_startServices, isInfrastructureHostsViewEnabled]: [
+ [CoreStart, InfraClientStartDeps, InfraClientStartExports],
+ boolean
+ ]) => {
+ this.appUpdater$.next(() => ({
+ deepLinks: isInfrastructureHostsViewEnabled
+ ? [hostInfraDeepLink, ...infraDeepLinks]
+ : infraDeepLinks,
+ }));
+ }
+ );
+
/* This exists purely to facilitate URL redirects from the old App ID ("infra"),
to our new App IDs ("metrics" and "logs"). With version 8.0.0 we can remove this. */
core.application.register({
diff --git a/x-pack/plugins/profiling/public/components/topn_functions.tsx b/x-pack/plugins/profiling/public/components/topn_functions.tsx
index 3ad540983d90..4d8522913a24 100644
--- a/x-pack/plugins/profiling/public/components/topn_functions.tsx
+++ b/x-pack/plugins/profiling/public/components/topn_functions.tsx
@@ -219,7 +219,7 @@ export const TopNFunctionsTable = ({
: row[sortField];
},
[sortDirection]
- ).slice(0, 100);
+ );
return (
<>
diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json
index 881f67dcf22c..6804a5ba5225 100644
--- a/x-pack/plugins/security/kibana.json
+++ b/x-pack/plugins/security/kibana.json
@@ -9,7 +9,7 @@
"kibanaVersion": "kibana",
"configPath": ["xpack", "security"],
"requiredPlugins": ["features", "licensing", "taskManager"],
- "optionalPlugins": ["dataViews", "home", "management", "usageCollection", "spaces", "share"],
+ "optionalPlugins": ["cloud", "dataViews", "home", "management", "usageCollection", "spaces", "share"],
"server": true,
"ui": true,
"enabledOnAnonymousPages": true,
diff --git a/x-pack/plugins/security/public/analytics/analytics_service.test.ts b/x-pack/plugins/security/public/analytics/analytics_service.test.ts
index 8174fffc250d..28a272c12f9e 100644
--- a/x-pack/plugins/security/public/analytics/analytics_service.test.ts
+++ b/x-pack/plugins/security/public/analytics/analytics_service.test.ts
@@ -11,6 +11,8 @@ import { coreMock } from '@kbn/core/public/mocks';
import { nextTick } from '@kbn/test-jest-helpers';
import { licenseMock } from '../../common/licensing/index.mock';
+import { authenticationMock } from '../authentication/index.mock';
+import { securityMock } from '../mocks';
import { AnalyticsService } from './analytics_service';
describe('AnalyticsService', () => {
@@ -29,7 +31,14 @@ describe('AnalyticsService', () => {
expect(localStorage.getItem(AnalyticsService.AuthTypeInfoStorageKey)).toBeNull();
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: true }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: true }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
@@ -51,7 +60,12 @@ describe('AnalyticsService', () => {
mockCore.http.post.mockResolvedValue({ signature: 'some-signature', timestamp: 1234 });
const licenseFeatures$ = new BehaviorSubject({ allowLogin: true });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
securityLicense: licenseMock.create(licenseFeatures$.asObservable()),
});
analyticsService.start({ http: mockCore.http });
@@ -99,7 +113,14 @@ describe('AnalyticsService', () => {
});
localStorage.setItem(AnalyticsService.AuthTypeInfoStorageKey, mockCurrentAuthTypeInfo);
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: true }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: true }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
@@ -117,7 +138,14 @@ describe('AnalyticsService', () => {
it('does not report authentication type if security is not available', async () => {
const mockCore = coreMock.createStart();
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: false }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: false }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
@@ -136,7 +164,14 @@ describe('AnalyticsService', () => {
});
localStorage.setItem(AnalyticsService.AuthTypeInfoStorageKey, mockCurrentAuthTypeInfo);
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: true }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: true }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
diff --git a/x-pack/plugins/security/public/analytics/analytics_service.ts b/x-pack/plugins/security/public/analytics/analytics_service.ts
index 1db3a9566f6f..87c402e9983a 100644
--- a/x-pack/plugins/security/public/analytics/analytics_service.ts
+++ b/x-pack/plugins/security/public/analytics/analytics_service.ts
@@ -9,12 +9,20 @@ import type { Subscription } from 'rxjs';
import { filter } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
-import type { HttpStart } from '@kbn/core/public';
+import type {
+ AnalyticsServiceSetup as CoreAnalyticsServiceSetup,
+ HttpStart,
+} from '@kbn/core/public';
+import type { AuthenticationServiceSetup } from '..';
import type { SecurityLicense } from '../../common';
+import { registerUserContext } from './register_user_context';
interface AnalyticsServiceSetupParams {
securityLicense: SecurityLicense;
+ analytics: CoreAnalyticsServiceSetup;
+ authc: AuthenticationServiceSetup;
+ cloudId?: string;
}
interface AnalyticsServiceStartParams {
@@ -35,8 +43,9 @@ export class AnalyticsService {
private securityLicense!: SecurityLicense;
private securityFeaturesSubscription?: Subscription;
- public setup({ securityLicense }: AnalyticsServiceSetupParams) {
+ public setup({ analytics, authc, cloudId, securityLicense }: AnalyticsServiceSetupParams) {
this.securityLicense = securityLicense;
+ registerUserContext(analytics, authc, cloudId);
}
public start({ http }: AnalyticsServiceStartParams) {
diff --git a/x-pack/plugins/security/public/analytics/register_user_context.test.ts b/x-pack/plugins/security/public/analytics/register_user_context.test.ts
new file mode 100644
index 000000000000..065404205964
--- /dev/null
+++ b/x-pack/plugins/security/public/analytics/register_user_context.test.ts
@@ -0,0 +1,126 @@
+/*
+ * 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 { firstValueFrom } from 'rxjs';
+
+import type { AnalyticsServiceSetup } from '@kbn/core/public';
+import { coreMock } from '@kbn/core/public/mocks';
+import { Sha256 } from '@kbn/crypto-browser';
+
+import type { AuthenticationServiceSetup } from '..';
+import { authenticationMock } from '../authentication/index.mock';
+import { securityMock } from '../mocks';
+import { registerUserContext } from './register_user_context';
+
+describe('registerUserContext', () => {
+ const username = '1234';
+ const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex');
+
+ let analytics: jest.Mocked;
+ let authentication: jest.Mocked;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ analytics = coreMock.createSetup().analytics;
+ authentication = authenticationMock.createSetup();
+ authentication.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+ });
+
+ test('register the context provider for the cloud user with hashed user ID when security is available', async () => {
+ registerUserContext(analytics, authentication, 'cloudId');
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: '7a3e98632e2c878671da5d5c49e625dd84fb4ba85758feae9a5fd5ec57724753',
+ isElasticCloudUser: false,
+ });
+ });
+
+ it('user hash includes cloud id', async () => {
+ authentication.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser({ username })
+ );
+ const analytics1 = coreMock.createSetup().analytics;
+ registerUserContext(analytics1, authentication, 'esOrg1');
+
+ const [{ context$: context1$ }] = analytics1.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ const { userId: hashId1 } = (await firstValueFrom(context1$)) as { userId: string };
+ expect(hashId1).not.toEqual(expectedHashedPlainUsername);
+
+ const analytics2 = coreMock.createSetup().analytics;
+ registerUserContext(analytics2, authentication, 'esOrg2');
+ const [{ context$: context2$ }] = analytics2.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === '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 () => {
+ authentication.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser({ username, elastic_cloud_user: true })
+ );
+ registerUserContext(analytics, authentication, 'cloudDeploymentId');
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: expectedHashedPlainUsername,
+ isElasticCloudUser: true,
+ });
+ });
+
+ test('user hash does not include cloudId when not provided', async () => {
+ authentication.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser({ username })
+ );
+ registerUserContext(analytics, authentication);
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: expectedHashedPlainUsername,
+ isElasticCloudUser: false,
+ });
+ });
+
+ test('user hash is undefined when failed to fetch a user', async () => {
+ authentication.getCurrentUser.mockRejectedValue(new Error('failed to fetch a user'));
+
+ registerUserContext(analytics, authentication);
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: undefined,
+ isElasticCloudUser: false,
+ });
+ });
+});
diff --git a/x-pack/plugins/security/public/analytics/register_user_context.ts b/x-pack/plugins/security/public/analytics/register_user_context.ts
new file mode 100644
index 000000000000..bc48846913d0
--- /dev/null
+++ b/x-pack/plugins/security/public/analytics/register_user_context.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { catchError, from, map, of } from 'rxjs';
+
+import type { AnalyticsServiceSetup } from '@kbn/core/public';
+import { Sha256 } from '@kbn/crypto-browser';
+
+import type { AuthenticationServiceSetup } from '..';
+
+/**
+ * Set up the Analytics context provider for the User information.
+ * @param analytics Core's Analytics service. The Setup contract.
+ * @param authc {@link AuthenticationServiceSetup} used to get the current user's information
+ * @param cloudId The Cloud Org ID.
+ * @private
+ */
+export function registerUserContext(
+ analytics: AnalyticsServiceSetup,
+ authc: AuthenticationServiceSetup,
+ cloudId?: string
+) {
+ analytics.registerContextProvider({
+ name: 'user_id',
+ context$: from(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.',
+ },
+ },
+ },
+ });
+}
+
+function sha256(str: string) {
+ return new Sha256().update(str, 'utf8').digest('hex');
+}
diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx
index a7ce4e855962..2a9147982406 100644
--- a/x-pack/plugins/security/public/plugin.tsx
+++ b/x-pack/plugins/security/public/plugin.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type {
CoreSetup,
CoreStart,
@@ -43,6 +44,7 @@ export interface PluginSetupDependencies {
home?: HomePublicPluginSetup;
management?: ManagementSetup;
share?: SharePluginSetup;
+ cloud?: CloudSetup;
}
export interface PluginStartDependencies {
@@ -51,6 +53,7 @@ export interface PluginStartDependencies {
management?: ManagementStart;
spaces?: SpacesPluginStart;
share?: SharePluginStart;
+ cloud?: CloudStart;
}
export class SecurityPlugin
@@ -81,7 +84,7 @@ export class SecurityPlugin
public setup(
core: CoreSetup,
- { home, licensing, management, share }: PluginSetupDependencies
+ { cloud, home, licensing, management, share }: PluginSetupDependencies
): SecurityPluginSetup {
const { license } = this.securityLicenseService.setup({ license$: licensing.license$ });
@@ -106,7 +109,12 @@ export class SecurityPlugin
securityApiClients: this.securityApiClients,
});
- this.analyticsService.setup({ securityLicense: license });
+ this.analyticsService.setup({
+ analytics: core.analytics,
+ authc: this.authc,
+ cloudId: cloud?.cloudId,
+ securityLicense: license,
+ });
accountManagementApp.create({
authc: this.authc,
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index 6c8b97b1e4b9..10e3b99484f5 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -124,18 +124,9 @@ describe('Security Plugin', () => {
"privilegeDeprecationsService": Object {
"getKibanaRolesByFeatureId": [Function],
},
- "setIsElasticCloudDeployment": [Function],
}
`);
});
-
- it('#setIsElasticCloudDeployment cannot be called twice', () => {
- const { setIsElasticCloudDeployment } = plugin.setup(mockCoreSetup, mockSetupDependencies);
- setIsElasticCloudDeployment();
- expect(() => setIsElasticCloudDeployment()).toThrowErrorMatchingInlineSnapshot(
- `"The Elastic Cloud deployment flag has been set already!"`
- );
- });
});
describe('start()', () => {
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 682c2eba56da..9e5724b6a393 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -8,6 +8,7 @@
import type { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
+import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { TypeOf } from '@kbn/config-schema';
import type {
CoreSetup,
@@ -88,12 +89,6 @@ export interface SecurityPluginSetup {
* Exposes services to access kibana roles per feature id with the GetDeprecationsContext
*/
privilegeDeprecationsService: PrivilegeDeprecationsService;
-
- /**
- * Sets the flag to indicate that Kibana is running inside an Elastic Cloud deployment. This flag is supposed to be
- * set by the Cloud plugin and can be only once.
- */
- setIsElasticCloudDeployment: () => void;
}
/**
@@ -123,6 +118,7 @@ export interface PluginSetupDependencies {
}
export interface PluginStartDependencies {
+ cloud?: CloudStart;
features: FeaturesPluginStart;
licensing: LicensingPluginStart;
taskManager: TaskManagerStartContract;
@@ -206,21 +202,6 @@ export class SecurityPlugin
return this.userProfileStart;
};
- /**
- * Indicates whether Kibana is running inside an Elastic Cloud deployment. Since circular plugin dependencies are
- * forbidden, this flag is supposed to be set by the Cloud plugin that already depends on the Security plugin.
- * @private
- */
- private isElasticCloudDeployment?: boolean;
- private readonly getIsElasticCloudDeployment = () => this.isElasticCloudDeployment === true;
- private readonly setIsElasticCloudDeployment = () => {
- if (this.isElasticCloudDeployment !== undefined) {
- throw new Error(`The Elastic Cloud deployment flag has been set already!`);
- }
-
- this.isElasticCloudDeployment = true;
- };
-
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
@@ -374,13 +355,12 @@ export class SecurityPlugin
license,
logger: this.logger.get('deprecations'),
}),
- setIsElasticCloudDeployment: this.setIsElasticCloudDeployment,
});
}
public start(
core: CoreStart,
- { features, licensing, taskManager, spaces }: PluginStartDependencies
+ { cloud, features, licensing, taskManager, spaces }: PluginStartDependencies
) {
this.logger.debug('Starting plugin');
@@ -413,7 +393,7 @@ export class SecurityPlugin
session,
applicationName: this.authorizationSetup!.applicationName,
kibanaFeatures: features.getKibanaFeatures(),
- isElasticCloudDeployment: this.getIsElasticCloudDeployment,
+ isElasticCloudDeployment: () => cloud?.isCloudEnabled === true,
});
this.authorizationService.start({
diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json
index e4566248efc4..68c43cf64e6b 100644
--- a/x-pack/plugins/security/tsconfig.json
+++ b/x-pack/plugins/security/tsconfig.json
@@ -8,6 +8,7 @@
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"references": [
+ { "path": "../cloud/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
index 15c89c8cd9c2..6de81d3e95a5 100644
--- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
@@ -58,10 +58,12 @@ export const validateEvents = {
afterEvent: schema.maybe(schema.string()),
}),
body: schema.object({
- timeRange: schema.object({
- from: schema.string(),
- to: schema.string(),
- }),
+ timeRange: schema.maybe(
+ schema.object({
+ from: schema.string(),
+ to: schema.string(),
+ })
+ ),
indexPatterns: schema.arrayOf(schema.string()),
filter: schema.maybe(schema.string()),
entityType: schema.maybe(schema.string()),
diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
index dde2a7f92b1e..d25fd440d1c2 100644
--- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
@@ -160,7 +160,7 @@ export const calculateEndpointAuthz = (
canWritePolicyManagement,
canReadPolicyManagement,
canWriteActionsLogManagement,
- canReadActionsLogManagement,
+ canReadActionsLogManagement: canReadActionsLogManagement && isPlatinumPlusLicense,
// Response Actions
canIsolateHost: canIsolateHost && isPlatinumPlusLicense,
canUnIsolateHost: canIsolateHost,
diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts
index aa2263b9b518..c2436f3f2de9 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts
@@ -28,12 +28,12 @@ describe('Analyze events view for alerts', () => {
waitForAlertsToPopulate();
});
- it('should render analyzer when button is clicked', () => {
+ it('should render when button is clicked', () => {
openAnalyzerForFirstAlertInTimeline();
cy.get(ANALYZER_NODE).first().should('be.visible');
});
- it(`should render an analyzer view and display
+ it(`should display
a toast indicating the date range of found events when a time range has 0 events in it`, () => {
const dateContainingZeroEvents = 'Jul 27, 2022 @ 00:00:00.000';
setStartDate(dateContainingZeroEvents);
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
index ebfae21d5a5e..5a2d192b9fd4 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
@@ -68,7 +68,6 @@ export interface NavTab {
}
export const securityNavKeys = [
SecurityPageName.alerts,
- SecurityPageName.responseActionsHistory,
SecurityPageName.blocklist,
SecurityPageName.detectionAndResponse,
SecurityPageName.case,
@@ -81,6 +80,7 @@ export const securityNavKeys = [
SecurityPageName.hosts,
SecurityPageName.network,
SecurityPageName.overview,
+ SecurityPageName.responseActionsHistory,
SecurityPageName.rules,
SecurityPageName.timelines,
SecurityPageName.trustedApps,
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
index 1055c98835d5..5a99df01e532 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
@@ -17,6 +17,7 @@ import { TestProviders } from '../../../mock';
import { CASES_FEATURE_ID } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
import { useTourContext } from '../../guided_onboarding';
+import { useUserPrivileges } from '../../user_privileges';
import {
noCasesPermissions,
readCasesCapabilities,
@@ -38,6 +39,9 @@ jest.mock('../../../hooks/use_experimental_features');
jest.mock('../../../utils/route/use_route_spy');
jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks');
jest.mock('../../guided_onboarding');
+jest.mock('../../user_privileges');
+
+const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
describe('useSecuritySolutionNavigation', () => {
const mockRouteSpy = [
@@ -56,6 +60,9 @@ describe('useSecuritySolutionNavigation', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
(useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy);
(useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(true);
+ mockUseUserPrivileges.mockImplementation(() => ({
+ endpointPrivileges: { canReadActionsLogManagement: true },
+ }));
(useTourContext as jest.Mock).mockReturnValue({ isTourShown: false });
const cases = mockCasesContract();
@@ -83,6 +90,10 @@ describe('useSecuritySolutionNavigation', () => {
});
});
+ afterEach(() => {
+ mockUseUserPrivileges.mockReset();
+ });
+
it('should create navigation config', async () => {
const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
() => useSecuritySolutionNavigation(),
@@ -117,6 +128,23 @@ describe('useSecuritySolutionNavigation', () => {
).toBeUndefined();
});
+ it('should omit response actions history if hook reports false', () => {
+ mockUseUserPrivileges.mockImplementation(() => ({
+ endpointPrivileges: { canReadActionsLogManagement: false },
+ }));
+ const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
+ () => useSecuritySolutionNavigation(),
+ { wrapper: TestProviders }
+ );
+ const items = result.current?.items;
+ expect(items).toBeDefined();
+ expect(
+ items!
+ .find((item) => item.id === 'manage')
+ ?.items?.find((item) => item.id === 'response_actions_history')
+ ).toBeUndefined();
+ });
+
describe('Permission gated routes', () => {
describe('cases', () => {
it('should display the cases navigation item when the user has read permissions', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
index dc15e371ba63..a4364c856452 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
@@ -21,6 +21,7 @@ import { SecurityPageName } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useGlobalQueryString } from '../../../utils/global_query_string';
+import { useUserPrivileges } from '../../user_privileges';
export const usePrimaryNavigationItems = ({
navTabs,
@@ -71,6 +72,8 @@ export const usePrimaryNavigationItems = ({
function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
const hasCasesReadPermissions = useGetUserCasesPermissions().read;
const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu();
+ const canSeeResponseActionsHistory =
+ useUserPrivileges().endpointPrivileges.canReadActionsLogManagement;
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
const uiCapabilities = useKibana().services.application.capabilities;
@@ -138,7 +141,9 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
? [navTabs[SecurityPageName.hostIsolationExceptions]]
: []),
navTabs[SecurityPageName.blocklist],
- navTabs[SecurityPageName.responseActionsHistory],
+ ...(canSeeResponseActionsHistory
+ ? [navTabs[SecurityPageName.responseActionsHistory]]
+ : []),
navTabs[SecurityPageName.cloudSecurityPostureBenchmarks],
],
},
@@ -156,6 +161,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
navTabs,
hasCasesReadPermissions,
canSeeHostIsolationExceptions,
+ canSeeResponseActionsHistory,
isPolicyListEnabled,
]
);
diff --git a/x-pack/plugins/security_solution/public/management/links.test.ts b/x-pack/plugins/security_solution/public/management/links.test.ts
index 09c47bc70095..c8166563428a 100644
--- a/x-pack/plugins/security_solution/public/management/links.test.ts
+++ b/x-pack/plugins/security_solution/public/management/links.test.ts
@@ -80,13 +80,30 @@ describe('links', () => {
expect(filteredLinks).toEqual(links);
});
+ it('it returns all but response actions history when no access privilege to either response actions history or HIE but have at least one HIE entry', async () => {
+ fakeHttpServices.get.mockResolvedValue({ total: 1 });
+ const filteredLinks = await getManagementFilteredLinks(
+ coreMockStarted,
+ getPlugins(['superuser'])
+ );
+ (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ expect(filteredLinks).toEqual({
+ ...links,
+ links: links.links?.filter((link) => link.id !== SecurityPageName.responseActionsHistory),
+ });
+ });
+
it('it returns filtered links when not having isolation permissions and no host isolation exceptions entry', async () => {
fakeHttpServices.get.mockResolvedValue({ total: 0 });
(licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
expect(filteredLinks).toEqual({
...links,
- links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions),
+ links: links.links?.filter(
+ (link) =>
+ link.id !== SecurityPageName.hostIsolationExceptions &&
+ link.id !== SecurityPageName.responseActionsHistory
+ ),
});
});
});
diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts
index 03cfee736def..12a904201a9c 100644
--- a/x-pack/plugins/security_solution/public/management/links.ts
+++ b/x-pack/plugins/security_solution/public/management/links.ts
@@ -226,7 +226,7 @@ export const links: LinkItem = {
],
};
-const getFilteredLinks = (linkIds: SecurityPageName[]) => ({
+const excludeLinks = (linkIds: SecurityPageName[]) => ({
...links,
links: links.links?.filter((link) => !linkIds.includes(link.id)),
});
@@ -249,19 +249,26 @@ export const getManagementFilteredLinks = async (
)
: getEndpointAuthzInitialState();
if (!privileges.canAccessEndpointManagement) {
- return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
+ return excludeLinks([
+ SecurityPageName.hostIsolationExceptions,
+ SecurityPageName.responseActionsHistory,
+ ]);
}
- if (!privileges.canIsolateHost) {
+ if (!privileges.canIsolateHost || !privileges.canReadActionsLogManagement) {
const hostIsolationExceptionsApiClientInstance = HostIsolationExceptionsApiClient.getInstance(
core.http
);
const summaryResponse = await hostIsolationExceptionsApiClientInstance.summary();
if (!summaryResponse.total) {
- return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
+ return excludeLinks([
+ SecurityPageName.hostIsolationExceptions,
+ SecurityPageName.responseActionsHistory,
+ ]);
}
+ return excludeLinks([SecurityPageName.responseActionsHistory]);
}
} catch {
- return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
+ return excludeLinks([SecurityPageName.hostIsolationExceptions]);
}
return links;
diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx
index dd06a838a26c..590b3786ece1 100644
--- a/x-pack/plugins/security_solution/public/management/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx
@@ -76,7 +76,8 @@ const ResponseActionsTelemetry = () => (
);
export const ManagementContainer = memo(() => {
- const { loading, canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges;
+ const { loading, canAccessEndpointManagement, canReadActionsLogManagement } =
+ useUserPrivileges().endpointPrivileges;
// Lets wait until we can verify permissions
if (loading) {
@@ -103,10 +104,12 @@ export const ManagementContainer = memo(() => {
component={HostIsolationExceptionsTelemetry}
/>
-
+ {canReadActionsLogManagement && (
+
+ )}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
index 49a2e8173476..60dc7bd29895 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
@@ -135,6 +135,13 @@ describe('Policy Form Layout', () => {
expect(saveButton).toHaveLength(1);
expect(saveButton.text()).toEqual('Save');
});
+ it('should display beta badge', async () => {
+ await asyncActions;
+ policyFormLayoutView.update();
+ const saveButton = policyFormLayoutView.find('EuiBetaBadge');
+ expect(saveButton).toHaveLength(1);
+ expect(saveButton.text()).toEqual('beta');
+ });
describe('when the save button is clicked', () => {
let saveButton: FindReactWrapperResponse;
let confirmModal: FindReactWrapperResponse;
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx
index 1b8e4f204015..984bc53a014e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx
@@ -88,7 +88,7 @@ const SUPPLEMENTAL_OPTIONS: ReadonlyArray {
return !config.linux.events.session_data;
},
- beta: false,
+ beta: true,
},
];
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
index 04e694b2cedb..719fdedb7354 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
@@ -17,6 +17,17 @@ import type {
ResolverSchema,
} from '../../../common/endpoint/types';
+function getRangeFilter(timeRange: TimeRange | undefined) {
+ return timeRange
+ ? {
+ timeRange: {
+ from: timeRange.from,
+ to: timeRange.to,
+ },
+ }
+ : [];
+}
+
/**
* The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead.
*/
@@ -34,7 +45,7 @@ export function dataAccessLayerFactory(
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
const response: ResolverPaginatedEvents = await context.services.http.post(
@@ -43,10 +54,7 @@ export function dataAccessLayerFactory(
query: {},
body: JSON.stringify({
indexPatterns,
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
filter: JSON.stringify({
bool: {
filter: [
@@ -76,16 +84,13 @@ export function dataAccessLayerFactory(
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
const commonFields = {
query: { afterEvent: after, limit: 25 },
body: {
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
indexPatterns,
},
};
@@ -127,30 +132,28 @@ export function dataAccessLayerFactory(
limit,
}: {
ids: string[];
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
limit: number;
}): Promise {
- const response: ResolverPaginatedEvents = await context.services.http.post(
- '/api/endpoint/resolver/events',
- {
- query: { limit },
- body: JSON.stringify({
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
+ const query = {
+ query: { limit },
+ body: JSON.stringify({
+ indexPatterns,
+ ...getRangeFilter(timeRange),
+ filter: JSON.stringify({
+ bool: {
+ filter: [
+ { terms: { 'process.entity_id': ids } },
+ { term: { 'event.category': 'process' } },
+ ],
},
- indexPatterns,
- filter: JSON.stringify({
- bool: {
- filter: [
- { terms: { 'process.entity_id': ids } },
- { term: { 'event.category': 'process' } },
- ],
- },
- }),
}),
- }
+ }),
+ };
+ const response: ResolverPaginatedEvents = await context.services.http.post(
+ '/api/endpoint/resolver/events',
+ query
);
return response.events;
},
@@ -172,7 +175,7 @@ export function dataAccessLayerFactory(
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
/** @description - eventID isn't provided by winlog. This can be removed once runtime fields are available */
@@ -200,10 +203,7 @@ export function dataAccessLayerFactory(
query: { limit: 1 },
body: JSON.stringify({
indexPatterns,
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
filter: JSON.stringify(filter),
}),
}
@@ -217,10 +217,7 @@ export function dataAccessLayerFactory(
query: { limit: 1 },
body: JSON.stringify({
indexPatterns,
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
entityType: 'alertDetail',
eventID,
}),
@@ -250,7 +247,7 @@ export function dataAccessLayerFactory(
}: {
dataId: string;
schema: ResolverSchema;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indices: string[];
ancestors: number;
descendants: number;
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts
index 130b81c5622b..6b833c93704b 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts
@@ -63,7 +63,7 @@ export function generateTreeWithDAL(
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
const node = allNodes.get(entityID);
@@ -88,7 +88,7 @@ export function generateTreeWithDAL(
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> {
const node = allNodes.get(entityID);
@@ -119,7 +119,7 @@ export function generateTreeWithDAL(
eventCategory: string[];
eventTimestamp: string;
eventID?: string | number;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return null;
@@ -135,7 +135,7 @@ export function generateTreeWithDAL(
limit,
}: {
ids: string[];
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
limit: number;
}): Promise {
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
index 000d08b4e15c..e883a96b162e 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
@@ -59,7 +59,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return Promise.resolve({
@@ -83,7 +83,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{
events: SafeResolverEvent[];
@@ -110,7 +110,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return null;
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
index 808c4463f3a8..c4c7fda097e8 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
@@ -64,7 +64,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return Promise.resolve({
@@ -90,7 +90,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{
events: SafeResolverEvent[];
@@ -121,7 +121,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return mockEndpointEvent({
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
index 774111baf165..30f7e07bf041 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
@@ -67,7 +67,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
/**
@@ -97,7 +97,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> {
const events =
@@ -129,7 +129,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null;
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts
index 7eb8c28a433e..dc7031acdbd9 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts
@@ -58,7 +58,7 @@ export function oneNodeWithPaginatedEvents(): {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
/**
@@ -86,7 +86,7 @@ export function oneNodeWithPaginatedEvents(): {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> {
let events: SafeResolverEvent[] = [];
@@ -121,7 +121,7 @@ export function oneNodeWithPaginatedEvents(): {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return mockTree.events.find((event) => eventModel.eventID(event) === eventID) ?? null;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts
index 6b58dd4e8e62..cd4119f9569e 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts
@@ -48,8 +48,9 @@ export function CurrentRelatedEventFetcher(
api.dispatch({
type: 'appRequestedCurrentRelatedEventData',
});
- const timeRangeFilters = selectors.timeRangeFilters(state);
-
+ const detectedBounds = selectors.detectedBounds(state);
+ const timeRangeFilters =
+ detectedBounds !== undefined ? undefined : selectors.timeRangeFilters(state);
let result: SafeResolverEvent | null = null;
try {
result = await dataAccessLayer.event({
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts
index c3173b323873..9a3a9eb3450f 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts
@@ -60,7 +60,9 @@ export function NodeDataFetcher(
let results: SafeResolverEvent[] | undefined;
try {
- const timeRangeFilters = selectors.timeRangeFilters(state);
+ const detectedBounds = selectors.detectedBounds(state);
+ const timeRangeFilters =
+ detectedBounds !== undefined ? undefined : selectors.timeRangeFilters(state);
results = await dataAccessLayer.nodeData({
ids: Array.from(newIDsToRequest),
timeRange: timeRangeFilters,
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
index ec0f068b5425..ab8f71940104 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
@@ -30,7 +30,9 @@ export function RelatedEventsFetcher(
const indices = selectors.eventIndices(state);
const oldParams = last;
- const timeRangeFilters = selectors.timeRangeFilters(state);
+ const detectedBounds = selectors.detectedBounds(state);
+ const timeRangeFilters =
+ detectedBounds !== undefined ? undefined : selectors.timeRangeFilters(state);
// Update this each time before fetching data (or even if we don't fetch data) so that subsequent actions that call this (concurrently) will have up to date info.
last = newParams;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
index e4da1af5f4d7..61319158fccc 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
@@ -93,9 +93,9 @@ export function ResolverTreeFetcher(
descendants: descendantsRequestAmount(),
});
if (unboundedTree.length > 0) {
- const timestamps = unboundedTree.map((event) =>
- firstNonNullValue(event.data['@timestamp'])
- );
+ const timestamps = unboundedTree
+ .map((event) => firstNonNullValue(event.data['@timestamp']))
+ .sort();
const oldestTimestamp = timestamps[0];
const newestTimestamp = timestamps.slice(-1);
api.dispatch({
diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts
index 00ecd995176e..88e97f416dc4 100644
--- a/x-pack/plugins/security_solution/public/resolver/types.ts
+++ b/x-pack/plugins/security_solution/public/resolver/types.ts
@@ -692,7 +692,7 @@ export interface DataAccessLayer {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}) => Promise;
@@ -710,7 +710,7 @@ export interface DataAccessLayer {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}) => Promise;
@@ -725,7 +725,7 @@ export interface DataAccessLayer {
limit,
}: {
ids: string[];
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
limit: number;
}): Promise;
@@ -747,7 +747,7 @@ export interface DataAccessLayer {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}) => Promise;
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
index ba4f68242367..869ae911ad89 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
@@ -11,31 +11,22 @@ import type { JsonObject, JsonValue } from '@kbn/utility-types';
import { parseFilterQuery } from '../../../../utils/serialized_query';
import type { SafeResolverEvent } from '../../../../../common/endpoint/types';
import type { PaginationBuilder } from '../utils/pagination';
-
-interface TimeRange {
- from: string;
- to: string;
-}
+import { BaseResolverQuery } from '../tree/queries/base';
+import type { ResolverQueryParams } from '../tree/queries/base';
/**
* Builds a query for retrieving events.
*/
-export class EventsQuery {
- private readonly pagination: PaginationBuilder;
- private readonly indexPatterns: string | string[];
- private readonly timeRange: TimeRange;
+export class EventsQuery extends BaseResolverQuery {
+ readonly pagination: PaginationBuilder;
constructor({
- pagination,
indexPatterns,
timeRange,
- }: {
- pagination: PaginationBuilder;
- indexPatterns: string | string[];
- timeRange: TimeRange;
- }) {
+ isInternalRequest,
+ pagination,
+ }: ResolverQueryParams & { pagination: PaginationBuilder }) {
+ super({ indexPatterns, timeRange, isInternalRequest });
this.pagination = pagination;
- this.indexPatterns = indexPatterns;
- this.timeRange = timeRange;
}
private query(filters: JsonObject[]): JsonObject {
@@ -44,15 +35,7 @@ export class EventsQuery {
bool: {
filter: [
...filters,
- {
- range: {
- '@timestamp': {
- gte: this.timeRange.from,
- lte: this.timeRange.to,
- format: 'strict_date_optional_time',
- },
- },
- },
+ ...this.getRangeFilter(),
{
term: { 'event.kind': 'event' },
},
@@ -71,15 +54,7 @@ export class EventsQuery {
{
term: { 'event.id': id },
},
- {
- range: {
- '@timestamp': {
- gte: this.timeRange.from,
- lte: this.timeRange.to,
- format: 'strict_date_optional_time',
- },
- },
- },
+ ...this.getRangeFilter(),
],
},
},
@@ -97,15 +72,7 @@ export class EventsQuery {
{
term: { 'process.entity_id': id },
},
- {
- range: {
- '@timestamp': {
- gte: this.timeRange.from,
- lte: this.timeRange.to,
- format: 'strict_date_optional_time',
- },
- },
- },
+ ...this.getRangeFilter(),
],
},
},
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts
index 6637e7931b05..256f2b58b686 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts
@@ -11,10 +11,10 @@ import type { TimeRange } from '../utils';
import { resolverFields } from '../utils';
export interface ResolverQueryParams {
- readonly schema: ResolverSchema;
+ readonly schema?: ResolverSchema;
readonly indexPatterns: string | string[];
readonly timeRange: TimeRange | undefined;
- readonly isInternalRequest: boolean;
+ readonly isInternalRequest?: boolean;
readonly resolverFields?: JsonValue[];
getRangeFilter?: () => Array<{
range: { '@timestamp': { gte: string; lte: string; format: string } };
@@ -25,12 +25,18 @@ export class BaseResolverQuery implements ResolverQueryParams {
readonly schema: ResolverSchema;
readonly indexPatterns: string | string[];
readonly timeRange: TimeRange | undefined;
- readonly isInternalRequest: boolean;
+ readonly isInternalRequest?: boolean;
readonly resolverFields?: JsonValue[];
constructor({ schema, indexPatterns, timeRange, isInternalRequest }: ResolverQueryParams) {
- this.resolverFields = resolverFields(schema);
- this.schema = schema;
+ const schemaOrDefault = schema
+ ? schema
+ : {
+ id: 'process.entity_id',
+ parent: 'process.parent.entity_id',
+ };
+ this.resolverFields = resolverFields(schemaOrDefault);
+ this.schema = schemaOrDefault;
this.indexPatterns = indexPatterns;
this.timeRange = timeRange;
this.isInternalRequest = isInternalRequest;
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx
index 4cee2eb9bfca..ed3f5499ba0a 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx
@@ -41,7 +41,7 @@ export const PingTimestamp = ({
label,
checkGroup,
stepStatus,
- allStepsLoaded,
+ allStepsLoaded = true,
initialStepNo = 1,
}: Props) => {
const [stepNumber, setStepNumber] = useState(initialStepNo);
diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts
index 668d97a0819e..ea269d87413e 100644
--- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts
+++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts
@@ -13,6 +13,8 @@ import { API_URLS } from '../../../common/constants';
import { getAllLocations } from '../../synthetics_service/get_all_locations';
import { ProjectMonitorFormatter } from '../../synthetics_service/project_monitor/project_monitor_formatter';
+const MAX_PAYLOAD_SIZE = 1048576 * 20; // 20MiB
+
export const addSyntheticsProjectMonitorRoute: SyntheticsStreamingRouteFactory = (
libs: UMServerLibs
) => ({
@@ -25,6 +27,11 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsStreamingRouteFactory =
monitors: schema.arrayOf(schema.any()),
}),
},
+ options: {
+ body: {
+ maxBytes: MAX_PAYLOAD_SIZE,
+ },
+ },
handler: async ({
request,
savedObjectsClient,
diff --git a/x-pack/plugins/synthetics/server/server.ts b/x-pack/plugins/synthetics/server/server.ts
index 12844c9cb922..7f667e0fb264 100644
--- a/x-pack/plugins/synthetics/server/server.ts
+++ b/x-pack/plugins/synthetics/server/server.ts
@@ -57,7 +57,7 @@ export const initSyntheticsServer = (
});
syntheticsAppStreamingApiRoutes.forEach((route) => {
- const { method, streamHandler, path } = syntheticsRouteWrapper(
+ const { method, streamHandler, path, options } = syntheticsRouteWrapper(
createSyntheticsRouteWithAuth(libs, route),
server,
syntheticsMonitorClient
@@ -82,7 +82,8 @@ export const initSyntheticsServer = (
};
},
method,
- server.router
+ server.router,
+ options
);
});
};
diff --git a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts
index 8706735fa925..fc1376e15760 100644
--- a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts
+++ b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts
@@ -19,6 +19,7 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
...uptimeRoute,
options: {
tags: ['access:uptime-read', ...(uptimeRoute?.writeAccess ? ['access:uptime-write'] : [])],
+ ...(uptimeRoute.options ?? {}),
},
streamHandler: async (context, request, subject) => {
const coreContext = await context.core;
diff --git a/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
index e52effa09ab3..c5d67894aa0f 100644
--- a/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
+++ b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
@@ -185,9 +185,7 @@ describe('Indicators', () => {
it('should render the inspector flyout', () => {
cy.get(INSPECTOR_BUTTON).last().click({ force: true });
- cy.get(INSPECTOR_PANEL).should('be.visible');
-
- cy.get(INSPECTOR_PANEL).contains('Index patterns');
+ cy.get(INSPECTOR_PANEL).contains('Indicators search requests');
});
});
});
diff --git a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
index 2bc1b704e815..0464e57c6749 100644
--- a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
+++ b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
@@ -31,7 +31,7 @@ export const FILTERS_GLOBAL_CONTAINER = '[data-test-subj="filters-global-contain
export const TIME_RANGE_PICKER = `[data-test-subj="superDatePickerToggleQuickMenuButton"]`;
-export const QUERY_INPUT = `[data-test-subj="iocListPageQueryInput"]`;
+export const QUERY_INPUT = `[data-test-subj="queryInput"]`;
export const EMPTY_STATE = '[data-test-subj="indicatorsTableEmptyState"]';
diff --git a/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx b/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx
index 6c7621977b8d..04ee12819d98 100644
--- a/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx
+++ b/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx
@@ -6,17 +6,23 @@
*/
import { EuiPageHeader, EuiPageHeaderSection, EuiSpacer, EuiText } from '@elastic/eui';
-import React, { FC } from 'react';
+import React, { FC, ReactNode } from 'react';
import { SecuritySolutionPageWrapper } from '../../containers/security_solution_page_wrapper';
export interface LayoutProps {
pageTitle?: string;
border?: boolean;
+ subHeader?: ReactNode;
}
export const TITLE_TEST_ID = 'tiDefaultPageLayoutTitle';
-export const DefaultPageLayout: FC = ({ children, pageTitle, border = true }) => {
+export const DefaultPageLayout: FC = ({
+ children,
+ pageTitle,
+ border = true,
+ subHeader,
+}) => {
return (
@@ -26,6 +32,12 @@ export const DefaultPageLayout: FC = ({ children, pageTitle, border
{pageTitle}
)}
+ {subHeader ? (
+ <>
+
+ {subHeader}
+ >
+ ) : null}
diff --git a/x-pack/plugins/threat_intelligence/public/components/update_status/index.ts b/x-pack/plugins/threat_intelligence/public/components/update_status/index.ts
new file mode 100644
index 000000000000..f83c0e64fda2
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/components/update_status/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './update_status';
diff --git a/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.test.tsx b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.test.tsx
new file mode 100644
index 000000000000..2ed1503d89a7
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { render } from '@testing-library/react';
+import React from 'react';
+import { TestProvidersComponent } from '../../common/mocks/test_providers';
+import { UpdateStatus } from './update_status';
+
+describe('', () => {
+ it('should render Updated now', () => {
+ const result = render(, {
+ wrapper: TestProvidersComponent,
+ });
+
+ expect(result.asFragment()).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
+
+ it('should render Updating when isUpdating', () => {
+ const result = render(, {
+ wrapper: TestProvidersComponent,
+ });
+
+ expect(result.asFragment()).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
+});
diff --git a/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.tsx b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.tsx
new file mode 100644
index 000000000000..02f43481186d
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedRelative } from '@kbn/i18n-react';
+
+interface UpdateStatusProps {
+ updatedAt: number;
+ isUpdating: boolean;
+}
+
+const UPDATING = i18n.translate('xpack.threatIntelligence.updateStatus.updating', {
+ defaultMessage: 'Updating...',
+});
+
+const UPDATED = i18n.translate('xpack.threatIntelligence.updateStatus.updated', {
+ defaultMessage: 'Updated',
+});
+
+export const UpdateStatus: React.FC = ({ isUpdating, updatedAt }) => (
+
+
+
+ {isUpdating ? (
+ UPDATING
+ ) : (
+ <>
+ {UPDATED}
+
+
+ >
+ )}
+
+
+
+);
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/__snapshots__/use_toolbar_options.test.tsx.snap b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/__snapshots__/use_toolbar_options.test.tsx.snap
new file mode 100644
index 000000000000..4b5868902333
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/__snapshots__/use_toolbar_options.test.tsx.snap
@@ -0,0 +1,120 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`useToolbarOptions() should return correct value for 0 indicators total 1`] = `
+Object {
+ "additionalControls": Object {
+ "left": Object {
+ "append": ,
+ "prepend":
+
+ -
+
+ ,
+ },
+ "right": ,
+ },
+ "showDisplaySelector": false,
+ "showFullScreenSelector": true,
+}
+`;
+
+exports[`useToolbarOptions() should return correct value for 25 indicators total 1`] = `
+Object {
+ "additionalControls": Object {
+ "left": Object {
+ "append": ,
+ "prepend":
+
+ Showing
+ 1
+ -
+ 25
+ of
+
+ 25
+ indicators
+
+ ,
+ },
+ "right": ,
+ },
+ "showDisplaySelector": false,
+ "showFullScreenSelector": true,
+}
+`;
+
+exports[`useToolbarOptions() should return correct value for 50 indicators total 1`] = `
+Object {
+ "additionalControls": Object {
+ "left": Object {
+ "append": ,
+ "prepend":
+
+ Showing
+ 26
+ -
+ 50
+ of
+
+ 50
+ indicators
+
+ ,
+ },
+ "right": ,
+ },
+ "showDisplaySelector": false,
+ "showFullScreenSelector": true,
+}
+`;
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx
index 084279fe8353..ecf1cbf0a477 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx
@@ -25,40 +25,7 @@ describe('useToolbarOptions()', () => {
{ wrapper: TestProvidersComponent }
);
- expect(result.result.current).toMatchInlineSnapshot(`
- Object {
- "additionalControls": Object {
- "left": Object {
- "append": ,
- "prepend":
-
- -
-
- ,
- },
- "right": ,
- },
- "showDisplaySelector": false,
- "showFullScreenSelector": false,
- }
- `);
+ expect(result.result.current).toMatchSnapshot();
});
it('should return correct value for 25 indicators total', () => {
@@ -76,47 +43,7 @@ describe('useToolbarOptions()', () => {
{ wrapper: TestProvidersComponent }
);
- expect(result.result.current).toMatchInlineSnapshot(`
- Object {
- "additionalControls": Object {
- "left": Object {
- "append": ,
- "prepend":
-
- Showing
- 1
- -
- 25
- of
-
- 25
- indicators
-
- ,
- },
- "right": ,
- },
- "showDisplaySelector": false,
- "showFullScreenSelector": false,
- }
- `);
+ expect(result.result.current).toMatchSnapshot();
});
it('should return correct value for 50 indicators total', () => {
@@ -134,46 +61,6 @@ describe('useToolbarOptions()', () => {
{ wrapper: TestProvidersComponent }
);
- expect(result.result.current).toMatchInlineSnapshot(`
- Object {
- "additionalControls": Object {
- "left": Object {
- "append": ,
- "prepend":
-
- Showing
- 26
- -
- 50
- of
-
- 50
- indicators
-
- ,
- },
- "right": ,
- },
- "showDisplaySelector": false,
- "showFullScreenSelector": false,
- }
- `);
+ expect(result.result.current).toMatchSnapshot();
});
});
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx
index b19d6df71463..12bd94951e33 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx
@@ -41,7 +41,7 @@ export const useToolbarOptions = ({
return useMemo(
() => ({
showDisplaySelector: false,
- showFullScreenSelector: false,
+ showFullScreenSelector: true,
additionalControls: {
left: {
prepend: (
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx
index 42f6a4eb1fdb..40d64636fa34 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx
@@ -105,6 +105,7 @@ describe('useIndicators()', () => {
expect(hookResult.result.current).toMatchInlineSnapshot(`
Object {
+ "dataUpdatedAt": 0,
"handleRefresh": [Function],
"indicatorCount": 0,
"indicators": Array [],
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts
index 2352f302a1d4..e2e0aaddf07a 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts
@@ -47,6 +47,8 @@ export interface UseIndicatorsValue {
* Data loading is in progress (see docs on `isFetching` here: https://tanstack.com/query/v4/docs/guides/queries)
*/
isFetching: boolean;
+
+ dataUpdatedAt: number;
}
export const useIndicators = ({
@@ -95,7 +97,7 @@ export const useIndicators = ({
[inspectorAdapters, searchService]
);
- const { isLoading, isFetching, data, refetch } = useQuery(
+ const { isLoading, isFetching, data, refetch, dataUpdatedAt } = useQuery(
[
'indicatorsTable',
{
@@ -132,5 +134,6 @@ export const useIndicators = ({
isLoading,
isFetching,
handleRefresh,
+ dataUpdatedAt,
};
};
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx
index e46c605d1a90..7f4db9fa7526 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx
@@ -42,6 +42,7 @@ describe('', () => {
onChangeItemsPerPage: stub,
onChangePage: stub,
handleRefresh: stub,
+ dataUpdatedAt: Date.now(),
});
(useFilters as jest.MockedFunction).mockReturnValue({
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx
index 511faaa73a7a..fcf690631d74 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx
@@ -20,6 +20,7 @@ import { useColumnSettings } from './components/indicators_table/hooks/use_colum
import { useAggregatedIndicators } from './hooks/use_aggregated_indicators';
import { IndicatorsFilters } from './containers/indicators_filters';
import { useSecurityContext } from '../../hooks/use_security_context';
+import { UpdateStatus } from '../../components/update_status';
const queryClient = new QueryClient();
@@ -48,6 +49,7 @@ const IndicatorsPageContent: VFC = () => {
pagination,
isLoading: isLoadingIndicators,
isFetching: isFetchingIndicators,
+ dataUpdatedAt,
} = useIndicators({
filters,
filterQuery,
@@ -72,10 +74,14 @@ const IndicatorsPageContent: VFC = () => {
return (
-
+ }
+ >
+
{
+ describe('disable', () => {
const objectRemover = new ObjectRemover(supertest);
after(() => objectRemover.removeAll());
@@ -110,21 +109,23 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
expect(response.body).to.eql('');
// task should still exist but be disabled
- const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
- expect(taskRecord2.type).to.eql('task');
- expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord2.task.params)).to.eql({
- alertId: createdAlert.id,
- spaceId: space.id,
- consumer: 'alertsFixture',
- });
- expect(taskRecord2.task.enabled).to.eql(false);
- // Ensure AAD isn't broken
- await checkAAD({
- supertest,
- spaceId: space.id,
- type: 'alert',
- id: createdAlert.id,
+ await retry.try(async () => {
+ const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
+ expect(taskRecord2.type).to.eql('task');
+ expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord2.task.params)).to.eql({
+ alertId: createdAlert.id,
+ spaceId: space.id,
+ consumer: 'alertsFixture',
+ });
+ expect(taskRecord2.task.enabled).to.eql(false);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
});
break;
default:
@@ -295,15 +296,17 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
// task should still exist but be disabled
- const taskRecord = await getScheduledTask(createdAlert.scheduled_task_id);
- expect(taskRecord.type).to.eql('task');
- expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord.task.params)).to.eql({
- alertId: createdAlert.id,
- spaceId: space.id,
- consumer: 'alerts',
+ await retry.try(async () => {
+ const taskRecord = await getScheduledTask(createdAlert.scheduled_task_id);
+ expect(taskRecord.type).to.eql('task');
+ expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord.task.params)).to.eql({
+ alertId: createdAlert.id,
+ spaceId: space.id,
+ consumer: 'alerts',
+ });
+ expect(taskRecord.task.enabled).to.eql(false);
});
- expect(taskRecord.task.enabled).to.eql(false);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
@@ -366,15 +369,17 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
// task should still exist but be disabled
- const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
- expect(taskRecord2.type).to.eql('task');
- expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord2.task.params)).to.eql({
- alertId: createdAlert.id,
- spaceId: space.id,
- consumer: 'alertsFixture',
+ await retry.try(async () => {
+ const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
+ expect(taskRecord2.type).to.eql('task');
+ expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord2.task.params)).to.eql({
+ alertId: createdAlert.id,
+ spaceId: space.id,
+ consumer: 'alertsFixture',
+ });
+ expect(taskRecord2.task.enabled).to.eql(false);
});
- expect(taskRecord2.task.enabled).to.eql(false);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts
index c89e5b48b236..b4cb36ab59d8 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts
@@ -235,8 +235,9 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F
// number of action executions broken down by connector type
expect(telemetry.count_actions_executions_by_type_per_day['test.throw'] > 0).to.be(true);
- // average execution time - just checking for non-zero as we can't set an exact number
- expect(telemetry.avg_execution_time_per_day > 0).to.be(true);
+ // average execution time - just checking for a positive number as we can't set an exact number
+ // if the time is less than 1ms it will round down to 0
+ expect(telemetry.avg_execution_time_per_day >= 0).to.be(true);
// average execution time broken down by rule type
expect(telemetry.avg_execution_time_by_type_per_day['test.throw'] > 0).to.be(true);
diff --git a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts
index b4bdf9f50beb..a2e1f158a73e 100644
--- a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts
+++ b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts
@@ -222,9 +222,7 @@ export default ({ getService }: FtrProviderContext) => {
const errorActions = data.filter((d) => d.type === expected.errorFilter);
expect(errorActions.length).to.be(1);
- expect(errorActions[0].payload).to.be(
- 'ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [does_not_exist]'
- );
+ expect(errorActions[0].payload).to.be('Failed to fetch field candidates.');
});
});
};
diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts
index 8185cb0f03a2..39d740663635 100644
--- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts
+++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts
@@ -145,8 +145,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
}
});
- // FLAKY: https://github.com/elastic/kibana/issues/136542
- describe.skip('spaces', () => {
+ describe('spaces', () => {
// the following tests create a user_1 which has uptime read access to space_1 and dashboard all access to space_2
const space1Id = 'space_1';
const space2Id = 'space_2';
diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts
index 9dce7e7d8fda..a8eec4c568dc 100644
--- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts
+++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts
@@ -19,7 +19,8 @@ import { PrivateLocationTestService } from './services/private_location_test_ser
import { comparePolicies, getTestProjectSyntheticsPolicy } from './sample_data/test_policy';
export default function ({ getService }: FtrProviderContext) {
- describe('AddProjectMonitors', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/142110
+ describe.skip('AddProjectMonitors', function () {
this.tags('skipCloud');
const supertest = getService('supertest');
diff --git a/x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts b/x-pack/test/common/lib/test_data_loader.ts
similarity index 79%
rename from x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts
rename to x-pack/test/common/lib/test_data_loader.ts
index 4b25c722603c..61c8ff4c1bf5 100644
--- a/x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts
+++ b/x-pack/test/common/lib/test_data_loader.ts
@@ -5,16 +5,14 @@
* 2.0.
*/
-import { FtrProviderContext } from '../ftr_provider_context';
-
-const SPACE_1 = {
+export const SPACE_1 = {
id: 'space_1',
name: 'Space 1',
description: 'This is the first test space',
disabledFeatures: [],
};
-const SPACE_2 = {
+export const SPACE_2 = {
id: 'space_2',
name: 'Space 2',
description: 'This is the second test space',
@@ -64,36 +62,38 @@ const OBJECTS_TO_SHARE: Array<{
},
];
-export function getTestDataLoader({ getService }: FtrProviderContext) {
+// @ts-ignore
+export function getTestDataLoader({ getService }) {
const spacesService = getService('spaces');
const kbnServer = getService('kibanaServer');
const supertest = getService('supertest');
const log = getService('log');
return {
- before: async () => {
+ createFtrSpaces: async () => {
await Promise.all([await spacesService.create(SPACE_1), await spacesService.create(SPACE_2)]);
},
- after: async () => {
+ deleteFtrSpaces: async () => {
await Promise.all([spacesService.delete(SPACE_1.id), spacesService.delete(SPACE_2.id)]);
},
- beforeEach: async () => {
+ createFtrSavedObjectsData: async (
+ spaceData: Array<{ spaceName: string | null; dataUrl: string }>
+ ) => {
log.debug('Loading test data for the following spaces: default, space_1 and space_2');
- await Promise.all([
- kbnServer.importExport.load(
- 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json'
- ),
- kbnServer.importExport.load(
- 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json',
- { space: SPACE_1.id }
- ),
- kbnServer.importExport.load(
- 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json',
- { space: SPACE_2.id }
- ),
- ]);
+
+ await Promise.all(
+ spaceData.map((spaceDataObj) => {
+ if (spaceDataObj.spaceName) {
+ return kbnServer.importExport.load(spaceDataObj.dataUrl, {
+ space: spaceDataObj.spaceName,
+ });
+ } else {
+ return kbnServer.importExport.load(spaceDataObj.dataUrl);
+ }
+ })
+ );
// Adjust spaces for the imported saved objects.
for (const { objects, spacesToAdd = [], spacesToRemove = [] } of OBJECTS_TO_SHARE) {
@@ -111,9 +111,9 @@ export function getTestDataLoader({ getService }: FtrProviderContext) {
}
},
- afterEach: async () => {
+ deleteFtrSavedObjectsData: async () => {
const allSpacesIds = [
- ...(await spacesService.getAll()).map((space) => space.id),
+ ...(await spacesService.getAll()).map((space: { id: string }) => space.id),
'non_existent_space',
];
log.debug(`Removing data from the following spaces: ${allSpacesIds.join(', ')}`);
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json
new file mode 100644
index 000000000000..9a2713fc6187
--- /dev/null
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json
@@ -0,0 +1,163 @@
+{
+ "attributes": {
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "defaultspace-index-pattern-id",
+ "migrationVersion": {
+ "index-pattern": "8.0.0"
+ },
+ "originId": "cts_ip_1",
+ "references": [],
+ "type": "index-pattern",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyOCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Count of requests",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "version": 1,
+ "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}",
+ "description": "",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"defaultspace-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "id": "defaultspace-isolatedtype-id",
+ "references": [],
+ "type": "isolatedtype",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Requests",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ }
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "defaultspace-dashboard-id",
+ "migrationVersion": {
+ "dashboard": "8.4.0"
+ },
+ "type": "dashboard",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyMCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A share-capable (isolated) saved-object only in the default space"
+ },
+ "id": "only_default_space",
+ "type": "sharecapabletype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "id": "all_spaces",
+ "type": "sharedtype",
+ "references": [],
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ5NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "My favorite global object"
+ },
+ "id": "globaltype-id",
+ "references": [],
+ "type": "globaltype",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object in the default and space_1 spaces"
+ },
+ "id": "default_and_space_1",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_1"
+ },
+ "id": "conflict_1",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_2a"
+ },
+ "id": "conflict_2a",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_2b"
+ },
+ "id": "conflict_2b",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_3"
+ },
+ "id": "conflict_3",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_4a"
+ },
+ "id": "conflict_4a",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Resolve outcome exactMatch"
+ },
+ "id": "exact-match",
+ "type": "resolvetype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Resolve outcome aliasMatch"
+ },
+ "id": "alias-match-newid",
+ "type": "resolvetype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json
new file mode 100644
index 000000000000..6356d5c01989
--- /dev/null
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json
@@ -0,0 +1,72 @@
+
+
+{
+ "attributes": {
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space1-index-pattern-id",
+ "migrationVersion": {
+ "index-pattern": "8.0.0"
+ },
+ "references": [],
+ "type": "index-pattern",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyOSwxXQ=="
+}
+
+{
+ "attributes": {
+ "description": "",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"space1-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ },
+ "title": "Count of requests",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "version": 1,
+ "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}"
+ },
+ "id": "space1-isolatedtype-id",
+ "references": [],
+ "type": "isolatedtype",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Requests",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ },
+ "version": 1
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space1-dashboard-id",
+ "migrationVersion": {
+ "dashboard": "8.4.0"
+ },
+ "type": "dashboard",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyMCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object only in space_1"
+ },
+ "id": "only_space_1",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A share-capable (isolated) saved-object only in space_1"
+ },
+ "id": "only_space_1",
+ "type": "sharecapabletype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json
new file mode 100644
index 000000000000..9715a5f54d2b
--- /dev/null
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json
@@ -0,0 +1,58 @@
+{
+ "attributes": {
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space2-index-pattern-id",
+ "migrationVersion": {
+ "index-pattern": "8.0.0"
+ },
+ "references": [],
+ "type": "index-pattern",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyOSwxXQ=="
+}
+
+{
+ "attributes": {
+ "description": "",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"space2-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ },
+ "title": "Count of requests",
+ "version": 1
+ },
+ "id": "space2-isolatedtype-id",
+ "references": [],
+ "type": "isolatedtype",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Requests",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ },
+ "version": 1
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space2-dashboard-id",
+ "migrationVersion": {
+ "dashboard": "8.4.0"
+ },
+ "type": "dashboard",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyMCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object only in space_2"
+ },
+ "id": "only_space_2",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
index 10709a6f2091..c9cb3b9739ee 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
@@ -6,11 +6,12 @@
*/
import expect from '@kbn/expect';
-import { SuperTest } from 'supertest';
+import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types';
+import type { FtrProviderContext } from '../ftr_provider_context';
export interface BulkGetTestDefinition extends TestDefinition {
request: Array<{ type: string; id: string }>;
@@ -33,7 +34,10 @@ const createRequest = ({ type, id, namespaces }: BulkGetTestCase) => ({
...(namespaces && { namespaces }), // individual "object namespaces" string array
});
-export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) {
+export function bulkGetTestSuiteFactory(context: FtrProviderContext) {
+ const testDataLoader = getTestDataLoader(context);
+ const supertest = context.getService('supertestWithoutAuth');
+
const expectSavedObjectForbidden = expectResponses.forbiddenTypes('bulk_get');
const expectResponseBody =
(testCases: BulkGetTestCase | BulkGetTestCase[], statusCode: 200 | 403): ExpectResponseBody =>
@@ -91,16 +95,31 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest {
- before(() =>
- esArchiver.load(
- 'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
- )
- );
- after(() =>
- esArchiver.unload(
- 'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
- )
- );
+ before(async () => {
+ await testDataLoader.createFtrSpaces();
+ await testDataLoader.createFtrSavedObjectsData([
+ {
+ spaceName: null,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json',
+ },
+ {
+ spaceName: SPACE_1.id,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json',
+ },
+ {
+ spaceName: SPACE_2.id,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json',
+ },
+ ]);
+ });
+
+ after(async () => {
+ await testDataLoader.deleteFtrSpaces();
+ await testDataLoader.deleteFtrSavedObjectsData();
+ });
for (const test of tests) {
it(`should return ${test.responseStatusCode} ${test.title}`, async () => {
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts
index 2c1fbf442b0e..ed251440d361 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts
@@ -67,14 +67,9 @@ const createTestCases = (spaceId: string) => {
return { normalTypes, crossNamespace, hiddenType, allTypes };
};
-export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertestWithoutAuth');
- const esArchiver = getService('esArchiver');
-
- const { addTests, createTestDefinitions, expectSavedObjectForbidden } = bulkGetTestSuiteFactory(
- esArchiver,
- supertest
- );
+export default function (context: FtrProviderContext) {
+ const { addTests, createTestDefinitions, expectSavedObjectForbidden } =
+ bulkGetTestSuiteFactory(context);
const createTests = (spaceId: string) => {
const { normalTypes, crossNamespace, hiddenType, allTypes } = createTestCases(spaceId);
// use singleRequest to reduce execution time and/or test combined cases
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts
index 41fa4749cc48..30ed220ea9ae 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts
@@ -55,11 +55,8 @@ const createTestCases = (spaceId: string) => [
{ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespaces: [ALL_SPACES_ID] },
];
-export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertest');
- const esArchiver = getService('esArchiver');
-
- const { addTests, createTestDefinitions } = bulkGetTestSuiteFactory(esArchiver, supertest);
+export default function (context: FtrProviderContext) {
+ const { addTests, createTestDefinitions } = bulkGetTestSuiteFactory(context);
const createTests = (spaceId: string) => {
const testCases = createTestCases(spaceId);
return createTestDefinitions(testCases, false, { singleRequest: true });
diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
index c781eff6d327..4c5ae878bbf6 100644
--- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
@@ -14,8 +14,8 @@ import {
} from '@kbn/core/server';
import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils';
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
-import { getTestDataLoader } from '../lib/test_data_loader';
-import { FtrProviderContext } from '../ftr_provider_context';
+import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader';
+import type { FtrProviderContext } from '../ftr_provider_context';
type TestResponse = Record;
@@ -74,6 +74,21 @@ const UUID_PATTERN = new RegExp(
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
);
+const SPACE_DATA_TO_LOAD: Array<{ spaceName: string | null; dataUrl: string }> = [
+ {
+ spaceName: null,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json',
+ },
+ {
+ spaceName: SPACE_1.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json',
+ },
+ {
+ spaceName: SPACE_2.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json',
+ },
+];
+
const getDestinationWithoutConflicts = () => 'space_2';
const getDestinationWithConflicts = (originSpaceId?: string) =>
!originSpaceId || originSpaceId === DEFAULT_SPACE_ID ? 'space_1' : DEFAULT_SPACE_ID;
@@ -748,16 +763,19 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
// test data only allows for the following spaces as the copy origin
expect(['default', 'space_1']).to.contain(spaceId);
- await testDataLoader.before();
+ await testDataLoader.createFtrSpaces();
});
after(async () => {
- await testDataLoader.after();
+ await testDataLoader.deleteFtrSpaces();
});
describe('single-namespace types', () => {
- beforeEach(async () => await testDataLoader.beforeEach());
- afterEach(async () => await testDataLoader.afterEach());
+ beforeEach(async () => {
+ await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD);
+ });
+
+ afterEach(async () => await testDataLoader.deleteFtrSavedObjectsData());
const dashboardObject = { type: 'dashboard', id: `cts_dashboard_${spaceId}` };
@@ -898,8 +916,8 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
const spaces = ['space_2'];
const includeReferences = false;
describe(`multi-namespace types with overwrite=${overwrite} and createNewCopies=${createNewCopies}`, () => {
- before(async () => await testDataLoader.beforeEach());
- after(async () => await testDataLoader.afterEach());
+ before(async () => await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD));
+ after(async () => await testDataLoader.deleteFtrSavedObjectsData());
const testCases = tests.multiNamespaceTestCases(overwrite, createNewCopies);
testCases.forEach(({ testTitle, objects, statusCode, response }) => {
diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
index 58a434bd0ca9..5f2c361714c4 100644
--- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
@@ -11,8 +11,8 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
import { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces';
import { getUrlPrefix } from '../lib/space_test_utils';
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
-import { FtrProviderContext } from '../ftr_provider_context';
-import { getTestDataLoader } from '../lib/test_data_loader';
+import type { FtrProviderContext } from '../ftr_provider_context';
+import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader';
type TestResponse = Record;
@@ -44,6 +44,21 @@ interface ResolveCopyToSpaceTestDefinition {
const NON_EXISTENT_SPACE_ID = 'non_existent_space';
+const SPACE_DATA_TO_LOAD: Array<{ spaceName: string | null; dataUrl: string }> = [
+ {
+ spaceName: null,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json',
+ },
+ {
+ spaceName: SPACE_1.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json',
+ },
+ {
+ spaceName: SPACE_2.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json',
+ },
+];
+
const getDestinationSpace = (originSpaceId?: string) => {
if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) {
return 'space_1';
@@ -487,8 +502,10 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
});
describe('single-namespace types', () => {
- beforeEach(async () => await testDataLoader.beforeEach());
- afterEach(async () => await testDataLoader.afterEach());
+ beforeEach(
+ async () => await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD)
+ );
+ afterEach(async () => await testDataLoader.deleteFtrSavedObjectsData());
const dashboardObject = { type: 'dashboard', id: `cts_dashboard_${spaceId}` };
const visualizationObject = { type: 'visualization', id: `cts_vis_3_${spaceId}` };
@@ -630,8 +647,8 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
const includeReferences = false;
const createNewCopies = false;
describe(`multi-namespace types with "overwrite" retry`, () => {
- before(async () => await testDataLoader.beforeEach());
- after(async () => await testDataLoader.afterEach());
+ before(async () => await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD));
+ after(async () => await testDataLoader.deleteFtrSavedObjectsData());
const testCases = tests.multiNamespaceTestCases();
testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => {