diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 9ac6c32772e4b..b2254c8fb1e05 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=12.19.0 +ARG NODE_VERSION=12.19.1 FROM node:${NODE_VERSION} AS base diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b7fb3ff04db71..af010089e4892 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -158,7 +158,7 @@ /packages/kbn-ui-shared-deps/ @elastic/kibana-operations /packages/kbn-es-archiver/ @elastic/kibana-operations /packages/kbn-utils/ @elastic/kibana-operations -/src/legacy/server/keystore/ @elastic/kibana-operations +/src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /vars/ @elastic/kibana-operations diff --git a/.node-version b/.node-version index 260a0e20f68fe..e9f788b12771f 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -12.19.0 +12.19.1 diff --git a/.nvmrc b/.nvmrc index 260a0e20f68fe..e9f788b12771f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.19.0 +12.19.1 diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md new file mode 100644 index 0000000000000..b30201f9e3991 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) + +## IFieldType.customName property + +Signature: + +```typescript +customName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 3ff2afafcc514..6f3876ff82f04 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -16,6 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-public.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-public.ifieldtype.count.md) | number | | +| [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) | string | | | [displayName](./kibana-plugin-plugins-data-public.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-public.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-public.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md new file mode 100644 index 0000000000000..c2e0b9bb855f4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [fieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md) + +## IndexPattern.fieldAttrs property + +Signature: + +```typescript +fieldAttrs: FieldAttrs; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md index 2c5f30e4889ea..a370341000960 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md @@ -10,6 +10,7 @@ Returns index pattern as saved object body for saving ```typescript getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -23,6 +24,7 @@ getAsSavedObjectBody(): { Returns: `{ + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md new file mode 100644 index 0000000000000..f81edf4b94b42 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) + +## IndexPattern.getFieldAttrs property + +Signature: + +```typescript +getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md index 349da63c13ca7..0c89a6a3d20ba 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md @@ -10,6 +10,7 @@ Get last saved saved object fields ```typescript getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 7e3192481dfff..1228bf7adc2ef 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -21,12 +21,14 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [deleteFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | +| [fieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md) | | FieldAttrs | | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | +| [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md new file mode 100644 index 0000000000000..6af981eb6996c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md) + +## IndexPatternAttributes.fieldAttrs property + +Signature: + +```typescript +fieldAttrs?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index 77a8ebb0b2d3f..c5ea38278e820 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -14,6 +14,7 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md) | string | | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-public.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md index 5d467a7a9cbce..e0abf8aeeaee6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPatternField` class Signature: ```typescript -constructor(spec: FieldSpec, displayName: string); +constructor(spec: FieldSpec); ``` ## Parameters @@ -17,5 +17,4 @@ constructor(spec: FieldSpec, displayName: string); | Parameter | Type | Description | | --- | --- | --- | | spec | FieldSpec | | -| displayName | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md new file mode 100644 index 0000000000000..ef8f9f1d31e4f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) + +## IndexPatternField.customName property + +Signature: + +```typescript +get customName(): string | undefined; + +set customName(label: string | undefined); +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md index c0ce2fff419bf..913d63c93e3c0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly displayName: string; +get displayName(): string; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 4f49a9a8fc3ab..ef99b4353a70b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -14,7 +14,7 @@ export declare class IndexPatternField implements IFieldType | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(spec, displayName)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | +| [(constructor)(spec)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | ## Properties @@ -23,6 +23,7 @@ export declare class IndexPatternField implements IFieldType | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | | [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | Description of field type conflicts across different indices in the same index pattern | | [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | Count is used for field popularity | +| [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) | | string | undefined | | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md index a6a3a5a093c8e..c7237701ae49d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md @@ -20,6 +20,7 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; + customName: string | undefined; }; ``` Returns: @@ -37,5 +38,6 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; + customName: string | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md new file mode 100644 index 0000000000000..e558c3ab19189 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md) + +## IndexPatternSpec.fieldAttrs property + +Signature: + +```typescript +fieldAttrs?: FieldAttrs; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md index f3b692209ca67..06917fcac1b4d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -14,6 +14,7 @@ export interface IndexPatternSpec | Property | Type | Description | | --- | --- | --- | +| [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md) | FieldAttrs | | | [fieldFormats](./kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md) | Record<string, SerializedFieldFormat> | | | [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | | [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md index ed365fe03f980..2a09d5b3adb1d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md @@ -9,5 +9,5 @@ Converts field array to map Signature: ```typescript -fieldArrayToMap: (fields: FieldSpec[]) => Record; +fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 57bb98de09ebd..48019fe410b97 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -22,7 +22,7 @@ export declare class IndexPatternsService | --- | --- | --- | --- | | [clearCache](./kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | | [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | -| [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[]) => Record<string, FieldSpec> | Converts field array to map | +| [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md new file mode 100644 index 0000000000000..f5fbc084237f2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) + +## IFieldType.customName property + +Signature: + +```typescript +customName?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index d106f3a35a91c..638700b1d24f8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -16,6 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-server.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-server.ifieldtype.count.md) | number | | +| [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) | string | | | [displayName](./kibana-plugin-plugins-data-server.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-server.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-server.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md new file mode 100644 index 0000000000000..c8bad55dee2e4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md) + +## IndexPattern.fieldAttrs property + +Signature: + +```typescript +fieldAttrs: FieldAttrs; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md index f1bdb2f729414..274a475872b0b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md @@ -10,6 +10,7 @@ Returns index pattern as saved object body for saving ```typescript getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -23,6 +24,7 @@ getAsSavedObjectBody(): { Returns: `{ + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md new file mode 100644 index 0000000000000..80dd329232ed8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) + +## IndexPattern.getFieldAttrs property + +Signature: + +```typescript +getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md index 324f9d0152ab5..9923c82f389ad 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md @@ -10,6 +10,7 @@ Get last saved saved object fields ```typescript getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 2e15c8d3867ec..3d2b021b29515 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -21,12 +21,14 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [deleteFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | +| [fieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md) | | FieldAttrs | | | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | +| [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) | | string[] | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md new file mode 100644 index 0000000000000..fded3ebac8b2c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [fieldAttrs](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md) + +## IndexPatternAttributes.fieldAttrs property + +Signature: + +```typescript +fieldAttrs?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index 40b029da00469..6559b4d7110be 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -14,6 +14,7 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldAttrs](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md) | string | | | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-server.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) | string | | diff --git a/package.json b/package.json index 2a0c292435403..b45789172cee9 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "**/typescript": "4.0.2" }, "engines": { - "node": "12.19.0", + "node": "12.19.1", "yarn": "^1.21.1" }, "dependencies": { @@ -247,7 +247,7 @@ "nock": "12.0.3", "node-fetch": "^2.6.1", "node-forge": "^0.10.0", - "nodemailer": "^4.7.0", + "nodemailer": "^6.4.16", "normalize-path": "^3.0.0", "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", @@ -500,7 +500,7 @@ "@types/node": "12.19.4", "@types/node-fetch": "^2.5.7", "@types/node-forge": "^0.9.5", - "@types/nodemailer": "^6.2.1", + "@types/nodemailer": "^6.4.0", "@types/normalize-path": "^3.0.0", "@types/object-hash": "^1.3.0", "@types/opn": "^5.1.0", diff --git a/src/legacy/server/keystore/errors.js b/src/cli/keystore/errors.js similarity index 100% rename from src/legacy/server/keystore/errors.js rename to src/cli/keystore/errors.js diff --git a/src/legacy/server/keystore/index.js b/src/cli/keystore/index.js similarity index 100% rename from src/legacy/server/keystore/index.js rename to src/cli/keystore/index.js diff --git a/src/legacy/server/keystore/keystore.js b/src/cli/keystore/keystore.js similarity index 100% rename from src/legacy/server/keystore/keystore.js rename to src/cli/keystore/keystore.js diff --git a/src/legacy/server/keystore/keystore.test.js b/src/cli/keystore/keystore.test.js similarity index 100% rename from src/legacy/server/keystore/keystore.test.js rename to src/cli/keystore/keystore.test.js diff --git a/src/cli/serve/read_keystore.js b/src/cli/keystore/read_keystore.js similarity index 95% rename from src/cli/serve/read_keystore.js rename to src/cli/keystore/read_keystore.js index 38d0e68bd5c4e..b3bca4cf11c39 100644 --- a/src/cli/serve/read_keystore.js +++ b/src/cli/keystore/read_keystore.js @@ -19,7 +19,7 @@ import { set } from '@elastic/safer-lodash-set'; -import { Keystore } from '../../legacy/server/keystore'; +import { Keystore } from '../keystore'; import { getKeystore } from '../../cli_keystore/get_keystore'; export function readKeystore(keystorePath = getKeystore()) { diff --git a/src/cli/serve/read_keystore.test.js b/src/cli/keystore/read_keystore.test.js similarity index 94% rename from src/cli/serve/read_keystore.test.js rename to src/cli/keystore/read_keystore.test.js index e5407b257a909..a35258febfb8e 100644 --- a/src/cli/serve/read_keystore.test.js +++ b/src/cli/keystore/read_keystore.test.js @@ -20,8 +20,8 @@ import path from 'path'; import { readKeystore } from './read_keystore'; -jest.mock('../../legacy/server/keystore'); -import { Keystore } from '../../legacy/server/keystore'; +jest.mock('../keystore'); +import { Keystore } from '../keystore'; describe('cli/serve/read_keystore', () => { beforeEach(() => { diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts index 0a2c90460430f..02692fb914fda 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.ts +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -82,7 +82,8 @@ function createConfigManager(configPath: string) { }; } -describe('Server logging configuration', function () { +// Failing: See https://github.com/elastic/kibana/issues/77279 +describe.skip('Server logging configuration', function () { let child: undefined | Child.ChildProcess; beforeEach(() => { diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index a1715cf3dba2c..f344d3b70ed9d 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -27,7 +27,7 @@ import { getConfigPath } from '@kbn/utils'; import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; import { fromRoot } from '../../core/server/utils'; import { bootstrap } from '../../core/server'; -import { readKeystore } from './read_keystore'; +import { readKeystore } from '../keystore/read_keystore'; function canRequire(path) { try { diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js index ba381ca2f3e14..74a72fe44d398 100644 --- a/src/cli_keystore/add.test.js +++ b/src/cli_keystore/add.test.js @@ -39,7 +39,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; import { PassThrough } from 'stream'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { add } from './add'; import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from './utils/prompt'; diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index d12c80b361c92..9fbea8f195122 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import { pkg } from '../core/server/utils'; import Command from '../cli/command'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { createCli } from './create'; import { listCli } from './list'; diff --git a/src/cli_keystore/create.test.js b/src/cli_keystore/create.test.js index cb85475eab1cb..346fa9e055129 100644 --- a/src/cli_keystore/create.test.js +++ b/src/cli_keystore/create.test.js @@ -38,7 +38,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { create } from './create'; import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from './utils/prompt'; diff --git a/src/cli_keystore/list.test.js b/src/cli_keystore/list.test.js index 11c474f908216..8da235a1932e6 100644 --- a/src/cli_keystore/list.test.js +++ b/src/cli_keystore/list.test.js @@ -36,7 +36,7 @@ jest.mock('fs', () => ({ })); import sinon from 'sinon'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli//keystore'; import { list } from './list'; import { Logger } from '../cli_plugin/lib/logger'; diff --git a/src/cli_keystore/remove.test.js b/src/cli_keystore/remove.test.js index fae8924c67287..fb700e6a8b9e2 100644 --- a/src/cli_keystore/remove.test.js +++ b/src/cli_keystore/remove.test.js @@ -30,7 +30,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { remove } from './remove'; describe('Kibana keystore', () => { diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 14adbced95c7a..1e69669e080ec 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -103,10 +103,6 @@ interface ListenerOptions { export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) { const server = new Server(serverOptions); - // remove fix + test as soon as update node.js to v12.19 https://github.com/elastic/kibana/pull/61587 - server.listener.headersTimeout = - listenerOptions.keepaliveTimeout + 2 * server.listener.headersTimeout; - server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; server.listener.setTimeout(listenerOptions.socketTimeout); server.listener.on('timeout', (socket) => { diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 274d7a4e5a488..4c833f5be6c5b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -237,6 +237,7 @@ kibana_vars=( xpack.security.authc.oidc.realm xpack.security.authc.saml.realm xpack.security.authc.saml.maxRedirectURLSize + xpack.security.authc.selector.enabled xpack.security.cookieName xpack.security.enabled xpack.security.encryptionKey diff --git a/src/plugins/charts/public/services/palettes/mock.ts b/src/plugins/charts/public/services/palettes/mock.ts index a7ec3cc16ce6f..2e45d93999b8e 100644 --- a/src/plugins/charts/public/services/palettes/mock.ts +++ b/src/plugins/charts/public/services/palettes/mock.ts @@ -22,7 +22,7 @@ import { PaletteService } from './service'; import { PaletteDefinition, SeriesLayer } from './types'; export const getPaletteRegistry = () => { - const mockPalette: jest.Mocked = { + const mockPalette1: jest.Mocked = { id: 'default', title: 'My Palette', getColor: jest.fn((_: SeriesLayer[]) => 'black'), @@ -41,9 +41,28 @@ export const getPaletteRegistry = () => { })), }; + const mockPalette2: jest.Mocked = { + id: 'mocked', + title: 'Mocked Palette', + getColor: jest.fn((_: SeriesLayer[]) => 'blue'), + getColors: jest.fn((num: number) => ['blue', 'yellow']), + toExpression: jest.fn(() => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'system_palette', + arguments: { + name: ['mocked'], + }, + }, + ], + })), + }; + return { - get: (_: string) => mockPalette, - getAll: () => [mockPalette], + get: (name: string) => (name !== 'default' ? mockPalette2 : mockPalette1), + getAll: () => [mockPalette1, mockPalette2], }; }; diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index 4279dd320ad62..afaa2d00d8cfd 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -47,6 +47,7 @@ Object { ], }, "count": 1, + "customName": undefined, "esTypes": Array [ "text", ], @@ -62,6 +63,7 @@ Object { "script": "script", "scripted": true, "searchable": true, + "shortDotsEnable": undefined, "subType": Object { "multi": Object { "parent": "parent", diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index c0eb55a15fead..7fe2a17124b78 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -22,7 +22,6 @@ import { IFieldType } from './types'; import { IndexPatternField } from './index_pattern_field'; import { FieldSpec, IndexPatternFieldMap } from '../types'; import { IndexPattern } from '../index_patterns'; -import { shortenDottedString } from '../../utils'; type FieldMap = Map; @@ -58,8 +57,7 @@ export const fieldList = ( this.groups.get(field.type)!.set(field.name, field); }; private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); - private calcDisplayName = (name: string) => - shortDotsEnable ? shortenDottedString(name) : name; + constructor() { super(); specs.map((field) => this.add(field)); @@ -71,7 +69,7 @@ export const fieldList = ( ...(this.groups.get(type) || new Map()).values(), ]; public readonly add = (field: FieldSpec) => { - const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + const newField = new IndexPatternField({ ...field, shortDotsEnable }); this.push(newField); this.setByName(newField); this.setByGroup(newField); @@ -86,7 +84,7 @@ export const fieldList = ( }; public readonly update = (field: FieldSpec) => { - const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + const newField = new IndexPatternField(field); const index = this.findIndex((f) => f.name === newField.name); this.splice(index, 1, newField); this.setByName(newField); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index be7836de31246..81c7d6b9b237b 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -28,7 +28,7 @@ describe('Field', function () { } function getField(values = {}) { - return new IndexPatternField({ ...fieldValues, ...values }, 'displayName'); + return new IndexPatternField({ ...fieldValues, ...values }); } const fieldValues = { @@ -150,12 +150,12 @@ describe('Field', function () { }); it('exports the property to JSON', () => { - const field = new IndexPatternField(fieldValues, 'displayName'); + const field = new IndexPatternField(fieldValues); expect(flatten(field)).toMatchSnapshot(); }); it('spec snapshot', () => { - const field = new IndexPatternField(fieldValues, 'displayName'); + const field = new IndexPatternField(fieldValues); const getFormatterForField = () => ({ toJSON: () => ({ diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 4a22508f7fef3..850c5a312fda1 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -21,16 +21,15 @@ import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; +import { shortenDottedString } from '../../utils'; export class IndexPatternField implements IFieldType { readonly spec: FieldSpec; // not writable or serialized - readonly displayName: string; private readonly kbnFieldType: KbnFieldType; - constructor(spec: FieldSpec, displayName: string) { + constructor(spec: FieldSpec) { this.spec = { ...spec, type: spec.name === '_source' ? '_source' : spec.type }; - this.displayName = displayName; this.kbnFieldType = getKbnFieldType(spec.type); } @@ -69,6 +68,14 @@ export class IndexPatternField implements IFieldType { this.spec.lang = lang; } + public get customName() { + return this.spec.customName; + } + + public set customName(label) { + this.spec.customName = label; + } + /** * Description of field type conflicts across different indices in the same index pattern */ @@ -85,6 +92,14 @@ export class IndexPatternField implements IFieldType { return this.spec.name; } + public get displayName(): string { + return this.spec.customName + ? this.spec.customName + : this.spec.shortDotsEnable + ? shortenDottedString(this.spec.name) + : this.spec.name; + } + public get type() { return this.spec.type; } @@ -140,7 +155,6 @@ export class IndexPatternField implements IFieldType { script: this.script, lang: this.lang, conflictDescriptions: this.conflictDescriptions, - name: this.name, type: this.type, esTypes: this.esTypes, @@ -149,6 +163,7 @@ export class IndexPatternField implements IFieldType { aggregatable: this.aggregatable, readFromDocValues: this.readFromDocValues, subType: this.subType, + customName: this.customName, }; } @@ -171,6 +186,8 @@ export class IndexPatternField implements IFieldType { readFromDocValues: this.readFromDocValues, subType: this.subType, format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, + customName: this.customName, + shortDotsEnable: this.spec.shortDotsEnable, }; } } diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 5814760601a67..86c22b0116ead 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -37,6 +37,7 @@ export interface IFieldType { scripted?: boolean; subType?: IFieldSubType; displayName?: string; + customName?: string; format?: any; toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index dc4da2456b47b..2741322acec0f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -2,12 +2,14 @@ exports[`IndexPattern toSpec should match snapshot 1`] = ` Object { + "fieldAttrs": Object {}, "fieldFormats": Object {}, "fields": Object { "@tags": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -23,6 +25,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -30,6 +33,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, + "customName": undefined, "esTypes": Array [ "date", ], @@ -45,6 +49,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, @@ -52,6 +57,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "_id", ], @@ -67,6 +73,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -74,6 +81,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "_source", ], @@ -89,6 +97,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "_source", }, @@ -96,6 +105,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "_type", ], @@ -111,6 +121,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -118,6 +129,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "geo_shape", ], @@ -133,6 +145,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "geo_shape", }, @@ -140,6 +153,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 10, + "customName": undefined, "esTypes": Array [ "long", ], @@ -155,6 +169,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "number", }, @@ -162,6 +177,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "conflict", ], @@ -177,6 +193,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "conflict", }, @@ -184,6 +201,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -199,6 +217,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -206,6 +225,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -221,6 +241,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": Object { "multi": Object { "parent": "extension", @@ -232,6 +253,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "geo_point", ], @@ -247,6 +269,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "geo_point", }, @@ -254,6 +277,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -269,6 +293,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -276,6 +301,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "murmur3", ], @@ -291,6 +317,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "murmur3", }, @@ -298,6 +325,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "ip", ], @@ -313,6 +341,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "ip", }, @@ -320,6 +349,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -335,6 +365,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -342,6 +373,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -357,6 +389,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": Object { "multi": Object { "parent": "machine.os", @@ -368,6 +401,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -383,6 +417,7 @@ Object { "script": undefined, "scripted": false, "searchable": false, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -390,6 +425,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -405,6 +441,7 @@ Object { "script": undefined, "scripted": false, "searchable": false, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -412,6 +449,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "integer", ], @@ -427,6 +465,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "number", }, @@ -434,6 +473,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "geo_point", ], @@ -449,6 +489,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "geo_point", }, @@ -456,6 +497,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "attachment", ], @@ -471,6 +513,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "attachment", }, @@ -478,6 +521,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "date", ], @@ -493,6 +537,7 @@ Object { "script": "1234", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, @@ -500,6 +545,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "murmur3", ], @@ -515,6 +561,7 @@ Object { "script": "1234", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "murmur3", }, @@ -522,6 +569,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "long", ], @@ -537,6 +585,7 @@ Object { "script": "1234", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "number", }, @@ -544,6 +593,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -559,6 +609,7 @@ Object { "script": "'i am a string'", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -566,6 +617,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 20, + "customName": undefined, "esTypes": Array [ "boolean", ], @@ -581,6 +633,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "boolean", }, @@ -588,6 +641,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, + "customName": undefined, "esTypes": Array [ "date", ], @@ -603,6 +657,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, @@ -610,6 +665,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "date", ], @@ -625,6 +681,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap index a3d19f311b765..c020e7595c565 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap @@ -2,6 +2,7 @@ exports[`IndexPatterns savedObjectToSpec 1`] = ` Object { + "fieldAttrs": Object {}, "fieldFormats": Object { "field": Object {}, }, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 4508d7b1d9082..c3a0c98745e21 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,6 +18,7 @@ */ import _, { each, reject } from 'lodash'; +import { FieldAttrs } from '../..'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -36,6 +37,7 @@ interface IndexPatternDeps { } interface SavedObjectBody { + fieldAttrs?: string; title?: string; timeFieldName?: string; intervalName?: string; @@ -70,6 +72,8 @@ export class IndexPattern implements IIndexPattern { private originalSavedObjectBody: SavedObjectBody = {}; private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; + // make private once manual field refresh is removed + public fieldAttrs: FieldAttrs; constructor({ spec = {}, @@ -101,10 +105,10 @@ export class IndexPattern implements IIndexPattern { this.title = spec.title || ''; this.timeFieldName = spec.timeFieldName; this.sourceFilters = spec.sourceFilters; - this.fields.replaceAll(Object.values(spec.fields || {})); this.type = spec.type; this.typeMeta = spec.typeMeta; + this.fieldAttrs = spec.fieldAttrs || {}; } setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { @@ -127,6 +131,20 @@ export class IndexPattern implements IIndexPattern { this.originalSavedObjectBody = this.getAsSavedObjectBody(); }; + getFieldAttrs = () => { + const newFieldAttrs = { ...this.fieldAttrs }; + + this.fields.forEach((field) => { + if (field.customName) { + newFieldAttrs[field.name] = { customName: field.customName }; + } else { + delete newFieldAttrs[field.name]; + } + }); + + return newFieldAttrs; + }; + getComputedFields() { const scriptFields: any = {}; if (!this.fields) { @@ -180,6 +198,7 @@ export class IndexPattern implements IIndexPattern { typeMeta: this.typeMeta, type: this.type, fieldFormats: this.fieldFormatMap, + fieldAttrs: this.fieldAttrs, }; } @@ -271,8 +290,10 @@ export class IndexPattern implements IIndexPattern { const fieldFormatMap = _.isEmpty(this.fieldFormatMap) ? undefined : JSON.stringify(this.fieldFormatMap); + const fieldAttrs = this.getFieldAttrs(); return { + fieldAttrs: fieldAttrs ? JSON.stringify(fieldAttrs) : undefined, title: this.title, timeFieldName: this.timeFieldName, intervalName: this.intervalName, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 19c6e9c7b8a7a..4f91079c1e139 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -35,6 +35,7 @@ import { GetFieldsOptions, IndexPatternSpec, IndexPatternAttributes, + FieldAttrs, FieldSpec, IndexPatternFieldMap, } from '../types'; @@ -249,7 +250,11 @@ export class IndexPatternsService { try { const fields = await this.getFieldsForIndexPattern(indexPattern); const scripted = indexPattern.getScriptedFields().map((field) => field.spec); - indexPattern.fields.replaceAll([...fields, ...scripted]); + const fieldAttrs = indexPattern.getFieldAttrs(); + const fieldsWithSavedAttrs = Object.values( + this.fieldArrayToMap([...fields, ...scripted], fieldAttrs) + ); + indexPattern.fields.replaceAll(fieldsWithSavedAttrs); } catch (err) { if (err instanceof IndexPatternMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); @@ -275,12 +280,13 @@ export class IndexPatternsService { fields: IndexPatternFieldMap, id: string, title: string, - options: GetFieldsOptions + options: GetFieldsOptions, + fieldAttrs: FieldAttrs = {} ) => { const scriptdFields = Object.values(fields).filter((field) => field.scripted); try { - const newFields = await this.getFieldsForWildcard(options); - return this.fieldArrayToMap([...newFields, ...scriptdFields]); + const newFields = (await this.getFieldsForWildcard(options)) as FieldSpec[]; + return this.fieldArrayToMap([...newFields, ...scriptdFields], fieldAttrs); } catch (err) { if (err instanceof IndexPatternMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); @@ -301,9 +307,9 @@ export class IndexPatternsService { * Converts field array to map * @param fields */ - fieldArrayToMap = (fields: FieldSpec[]) => + fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) => fields.reduce((collector, field) => { - collector[field.name] = field; + collector[field.name] = { ...field, customName: fieldAttrs?.[field.name]?.customName }; return collector; }, {}); @@ -325,6 +331,7 @@ export class IndexPatternsService { fieldFormatMap, typeMeta, type, + fieldAttrs, }, } = savedObject; @@ -332,6 +339,7 @@ export class IndexPatternsService { const parsedTypeMeta = typeMeta ? JSON.parse(typeMeta) : undefined; const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {}; const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : []; + const parsedFieldAttrs: FieldAttrs = fieldAttrs ? JSON.parse(fieldAttrs) : {}; return { id, @@ -340,10 +348,11 @@ export class IndexPatternsService { intervalName, timeFieldName, sourceFilters: parsedSourceFilters, - fields: this.fieldArrayToMap(parsedFields), + fields: this.fieldArrayToMap(parsedFields, parsedFieldAttrs), typeMeta: parsedTypeMeta, type, fieldFormats: parsedFieldFormatMap, + fieldAttrs: parsedFieldAttrs, }; }; @@ -369,17 +378,26 @@ export class IndexPatternsService { const spec = this.savedObjectToSpec(savedObject); const { title, type, typeMeta } = spec; + spec.fieldAttrs = savedObject.attributes.fieldAttrs + ? JSON.parse(savedObject.attributes.fieldAttrs) + : {}; const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields); let isSaveRequired = isFieldRefreshRequired; try { spec.fields = isFieldRefreshRequired - ? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, { - pattern: title as string, - metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), - type, - rollupIndex: typeMeta?.params?.rollupIndex, - }) + ? await this.refreshFieldSpecMap( + spec.fields || {}, + id, + spec.title as string, + { + pattern: title as string, + metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), + type, + rollupIndex: typeMeta?.params?.rollupIndex, + }, + spec.fieldAttrs + ) : spec.fields; } catch (err) { isSaveRequired = false; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index b381cc0963333..22c400562f6d4 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -48,6 +48,11 @@ export interface IndexPatternAttributes { intervalName?: string; sourceFilters?: string; fieldFormatMap?: string; + fieldAttrs?: string; +} + +export interface FieldAttrs { + [key: string]: { customName: string }; } export type OnNotification = (toastInputFields: ToastInputFields) => void; @@ -155,7 +160,6 @@ export interface FieldSpec { lang?: string; conflictDescriptions?: Record; format?: SerializedFieldFormat; - name: string; type: string; esTypes?: string[]; @@ -165,6 +169,9 @@ export interface FieldSpec { readFromDocValues?: boolean; subType?: IFieldSubType; indexed?: boolean; + customName?: string; + // not persisted + shortDotsEnable?: boolean; } export type IndexPatternFieldMap = Record; @@ -180,6 +187,7 @@ export interface IndexPatternSpec { typeMeta?: TypeMeta; type?: string; fieldFormats?: Record; + fieldAttrs?: FieldAttrs; } export interface SourceFilter { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 78b974758f8c0..0768658e40299 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -975,6 +975,8 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) + customName?: string; + // (undocumented) displayName?: string; // (undocumented) esTypes?: string[]; @@ -1096,6 +1098,10 @@ export class IndexPattern implements IIndexPattern { addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) deleteFieldFormat: (fieldName: string) => void; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fieldAttrs: FieldAttrs; // (undocumented) fieldFormatMap: Record; // (undocumented) @@ -1121,6 +1127,7 @@ export class IndexPattern implements IIndexPattern { time_zone?: string | undefined; }>> | undefined; getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -1140,12 +1147,19 @@ export class IndexPattern implements IIndexPattern { }[]; }; // (undocumented) + getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; + // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; // (undocumented) getNonScriptedFields(): IndexPatternField[]; getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; @@ -1210,6 +1224,8 @@ export type IndexPatternAggRestrictions = Record | undefined; @@ -1240,7 +1256,10 @@ export class IndexPatternField implements IFieldType { get count(): number; set count(count: number); // (undocumented) - readonly displayName: string; + get customName(): string | undefined; + set customName(label: string | undefined); + // (undocumented) + get displayName(): string; // (undocumented) get esTypes(): string[] | undefined; // (undocumented) @@ -1277,6 +1296,7 @@ export class IndexPatternField implements IFieldType { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; + customName: string | undefined; }; // (undocumented) toSpec({ getFormatterForField, }?: { @@ -1324,6 +1344,8 @@ export type IndexPatternSelectProps = Required, 'isLo // // @public (undocumented) export interface IndexPatternSpec { + // (undocumented) + fieldAttrs?: FieldAttrs; // (undocumented) fieldFormats?: Record; // (undocumented) @@ -1361,7 +1383,7 @@ export class IndexPatternsService { // // (undocumented) ensureDefaultIndexPattern: EnsureDefaultIndexPattern; - fieldArrayToMap: (fields: FieldSpec[]) => Record; + fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; get: (id: string) => Promise; // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts // @@ -2325,7 +2347,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:113:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index bb7a8f58c926c..b2db4f5c74729 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -507,6 +507,8 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) + customName?: string; + // (undocumented) displayName?: string; // (undocumented) esTypes?: string[]; @@ -557,6 +559,10 @@ export class IndexPattern implements IIndexPattern { addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) deleteFieldFormat: (fieldName: string) => void; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fieldAttrs: FieldAttrs; // (undocumented) fieldFormatMap: Record; // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts @@ -584,6 +590,7 @@ export class IndexPattern implements IIndexPattern { time_zone?: string | undefined; }>> | undefined; getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -603,6 +610,12 @@ export class IndexPattern implements IIndexPattern { }[]; }; // (undocumented) + getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; + // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; @@ -611,6 +624,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getNonScriptedFields(): IndexPatternField[]; getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; @@ -669,6 +683,8 @@ export class IndexPattern implements IIndexPattern { // // @public (undocumented) export interface IndexPatternAttributes { + // (undocumented) + fieldAttrs?: string; // (undocumented) fieldFormatMap?: string; // (undocumented) @@ -1195,8 +1211,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:56:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx index bd48b1e083871..b456fa0773b85 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx @@ -17,7 +17,6 @@ * under the License. */ import { IndexPattern } from '../../../../../kibana_services'; -import { shortenDottedString } from '../../../../helpers'; export type SortOrder = [string, string]; export interface ColumnProps { @@ -67,7 +66,7 @@ export function getDisplayedColumns( const field = indexPattern.getFieldByName(column); return { name: column, - displayName: isShortDots ? shortenDottedString(column) : column, + displayName: field ? field.displayName : column, isSortable: field && field.sortable ? true : false, isRemoveable: column !== '_source' || columns.length > 1, colLeftIdx: idx - 1 < 0 ? -1 : idx - 1, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx index 3d5698e2e0d96..7636939194ce1 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx @@ -35,6 +35,7 @@ function getMockIndexPattern() { if (name === 'test1') { return { name, + displayName: name, type: 'string', aggregatable: false, searchable: true, @@ -43,6 +44,7 @@ function getMockIndexPattern() { } else { return { name, + displayName: name, type: 'string', aggregatable: false, searchable: true, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx index ac986fcaf0cbc..08a2d07d0b8e0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx @@ -24,7 +24,7 @@ import { SortOrder } from './helpers'; interface Props { colLeftIdx: number; // idx of the column to the left, -1 if moving is not possible colRightIdx: number; // idx of the column to the right, -1 if moving is not possible - displayName: string; + displayName?: string; isRemoveable: boolean; isSortable: boolean; name: string; diff --git a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap index d00a956b7c73d..2fa96f9372380 100644 --- a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` +exports[`FieldName renders a geo field 1`] = `
@@ -24,7 +24,7 @@ exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - t.t.test + test.test.test
diff --git a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx index e6cf8a57686f1..0deddce1c40a8 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx @@ -32,9 +32,7 @@ test('FieldName renders a number field by providing a field record, useShortDots expect(component).toMatchSnapshot(); }); -test('FieldName renders a geo field, useShortDots is set to true', () => { - const component = render( - - ); +test('FieldName renders a geo field', () => { + const component = render(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/discover/public/application/components/field_name/field_name.tsx b/src/plugins/discover/public/application/components/field_name/field_name.tsx index cf11f971ef76c..b8f664d6cf38a 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.tsx @@ -18,30 +18,31 @@ */ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; - import { FieldIcon, FieldIconProps } from '../../../../../kibana_react/public'; -import { shortenDottedString } from '../../helpers'; import { getFieldTypeName } from './field_type_name'; +import { FieldMapping } from '../../doc_views/doc_views_types'; // properties fieldType and fieldName are provided in kbn_doc_view // this should be changed when both components are deangularized interface Props { fieldName: string; fieldType: string; - useShortDots?: boolean; + fieldMapping?: FieldMapping; fieldIconProps?: Omit; scripted?: boolean; } export function FieldName({ fieldName, + fieldMapping, fieldType, - useShortDots, fieldIconProps, scripted = false, }: Props) { const typeName = getFieldTypeName(fieldType); - const displayName = useShortDots ? shortenDottedString(fieldName) : fieldName; + const displayName = + fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : fieldName; + const tooltip = displayName !== fieldName ? `${fieldName} (${displayName})` : fieldName; return ( @@ -51,7 +52,7 @@ export function FieldName({ diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 02ed17cd01f07..391e15485f074 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -43,8 +43,6 @@ jest.mock('../../../kibana_services', () => ({ get: (key: string) => { if (key === 'fields:popularLimit') { return 5; - } else if (key === 'shortDots:enable') { - return false; } }, }, @@ -54,7 +52,6 @@ jest.mock('../../../kibana_services', () => ({ function getComponent({ selected = false, showDetails = false, - useShortDots = false, field, }: { selected?: boolean; @@ -72,19 +69,16 @@ function getComponent({ const finalField = field ?? - new IndexPatternField( - { - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'bytes' - ); + new IndexPatternField({ + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const props = { indexPattern, @@ -95,7 +89,6 @@ function getComponent({ onRemoveField: jest.fn(), showDetails, selected, - useShortDots, }; const comp = mountWithIntl(); return { comp, props }; @@ -118,17 +111,14 @@ describe('discover sidebar field', function () { expect(props.getDetails).toHaveBeenCalledWith(props.field); }); it('should not allow clicking on _source', function () { - const field = new IndexPatternField( - { - name: '_source', - type: '_source', - esTypes: ['_source'], - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - '_source' - ); + const field = new IndexPatternField({ + name: '_source', + type: '_source', + esTypes: ['_source'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const { comp, props } = getComponent({ selected: true, field, diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 0329b3a34580c..35515a6a0e7a5 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -24,7 +24,6 @@ import { DiscoverFieldDetails } from './discover_field_details'; import { FieldIcon, FieldButton } from '../../../../../kibana_react/public'; import { FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; -import { shortenDottedString } from '../../helpers'; import { getFieldTypeName } from './lib/get_field_type_name'; import './discover_field.scss'; @@ -58,10 +57,6 @@ export interface DiscoverFieldProps { * Determines whether the field is selected */ selected?: boolean; - /** - * Determines whether the field name is shortened test.sub1.sub2 = t.s.sub2 - */ - useShortDots?: boolean; /** * Metric tracking function * @param metricType @@ -78,7 +73,6 @@ export function DiscoverField({ onAddFilter, getDetails, selected, - useShortDots, trackUiMetric, }: DiscoverFieldProps) { const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { @@ -118,13 +112,12 @@ export function DiscoverField({ ); + const title = + field.displayName !== field.name ? `${field.name} (${field.displayName} )` : field.displayName; + const fieldName = ( - - {useShortDots ? wrapOnDot(shortenDottedString(field.name)) : wrapOnDot(field.displayName)} + + {wrapOnDot(field.displayName)} ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx index 8607873b98d3d..0618e53d15dbb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -48,55 +48,46 @@ describe('discover sidebar field details', function () { } it('should enable the visualize link for a number field', function () { - const visualizableField = new IndexPatternField( - { - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'bytes' - ); + const visualizableField = new IndexPatternField({ + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const comp = mountComponent(visualizableField); expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy(); }); it('should disable the visualize link for an _id field', function () { - const conflictField = new IndexPatternField( - { - name: '_id', - type: 'string', - esTypes: ['_id'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'test' - ); + const conflictField = new IndexPatternField({ + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const comp = mountComponent(conflictField); expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({}); }); it('should disable the visualize link for an unknown field', function () { - const unknownField = new IndexPatternField( - { - name: 'test', - type: 'unknown', - esTypes: ['double'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'test' - ); + const unknownField = new IndexPatternField({ + name: 'test', + type: 'unknown', + esTypes: ['double'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const comp = mountComponent(unknownField); expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({}); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 7504d181d82b2..23d2fa0a39f34 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -51,8 +51,6 @@ jest.mock('../../../kibana_services', () => ({ get: (key: string) => { if (key === 'fields:popularLimit') { return 5; - } else if (key === 'shortDots:enable') { - return false; } }, }, diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index dfd09ccee9337..b8e09ce4d17e8 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -30,7 +30,7 @@ import { IndexPatternAttributes } from '../../../../../data/common'; import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; -import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; @@ -117,7 +117,6 @@ export function DiscoverSidebar({ ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); - const useShortDots = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const { selected: selectedFields, @@ -201,7 +200,6 @@ export function DiscoverSidebar({ onAddFilter={onAddFilter} getDetails={getDetailsByField} selected={true} - useShortDots={useShortDots} trackUiMetric={trackUiMetric} /> @@ -276,7 +274,6 @@ export function DiscoverSidebar({ onRemoveField={onRemoveField} onAddFilter={onAddFilter} getDetails={getDetailsByField} - useShortDots={useShortDots} trackUiMetric={trackUiMetric} /> @@ -307,7 +304,6 @@ export function DiscoverSidebar({ onRemoveField={onRemoveField} onAddFilter={onAddFilter} getDetails={getDetailsByField} - useShortDots={useShortDots} trackUiMetric={trackUiMetric} /> diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts index eb139f97c7b00..ebbffae83125c 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts @@ -59,6 +59,7 @@ describe('field_filter', function () { const fieldList = [ { name: 'bytes', + displayName: 'Bye,bye,Bytes', type: 'number', esTypes: ['long'], count: 10, @@ -68,6 +69,7 @@ describe('field_filter', function () { }, { name: 'extension', + displayName: 'Extension', type: 'string', esTypes: ['text'], count: 10, @@ -80,6 +82,8 @@ describe('field_filter', function () { [ { filter: {}, result: ['bytes', 'extension'] }, { filter: { name: 'by' }, result: ['bytes'] }, + { filter: { name: 'Ext' }, result: ['extension'] }, + { filter: { name: 'Bytes' }, result: ['bytes'] }, { filter: { aggregatable: true }, result: ['extension'] }, { filter: { aggregatable: true, searchable: false }, result: [] }, { filter: { type: 'string' }, result: ['extension'] }, diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts index f0d9a2d8af20f..2e1d9b76606df 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts @@ -72,7 +72,9 @@ export function isFieldFiltered( field.type === '_source' || field.scripted || fieldCounts[field.name] > 0; - const matchName = !filterState.name || field.name.indexOf(filterState.name) !== -1; + const needle = filterState.name ? filterState.name.toLowerCase() : ''; + const haystack = `${field.name}${field.displayName || ''}`.toLowerCase(); + const matchName = !filterState.name || haystack.indexOf(needle) !== -1; return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName; } diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 628045bd32f61..5d37f598b38f6 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -46,7 +46,13 @@ export function DocViewTable({ {Object.keys(flattened) - .sort() + .sort((fieldA, fieldB) => { + const mappingA = mapping(fieldA); + const mappingB = mapping(fieldB); + const nameA = !mappingA || !mappingA.displayName ? fieldA : mappingA.displayName; + const nameB = !mappingB || !mappingB.displayName ? fieldB : mappingB.displayName; + return nameA.localeCompare(nameB); + }) .map((field) => { const valueRaw = flattened[field]; const value = trimAngularSpan(String(formatted[field])); diff --git a/src/plugins/discover/public/application/components/table/table_row.tsx b/src/plugins/discover/public/application/components/table/table_row.tsx index 5f7dd9f37dcd3..3d75e175951d5 100644 --- a/src/plugins/discover/public/application/components/table/table_row.tsx +++ b/src/plugins/discover/public/application/components/table/table_row.tsx @@ -91,6 +91,7 @@ export function DocViewTableRow({ diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 6c90861e26727..01145402e0f29 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -36,6 +36,7 @@ export interface FieldMapping { rowCount?: number; type: string; name: string; + displayName?: string; } export type DocViewFilterFn = ( diff --git a/src/plugins/discover/public/application/helpers/index.ts b/src/plugins/discover/public/application/helpers/index.ts index 3555d24924e80..f7497c29a2bda 100644 --- a/src/plugins/discover/public/application/helpers/index.ts +++ b/src/plugins/discover/public/application/helpers/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { shortenDottedString } from './shorten_dotted_string'; export { formatNumWithCommas } from './format_number_with_commas'; diff --git a/src/plugins/discover/public/application/helpers/shorten_dotted_string.ts b/src/plugins/discover/public/application/helpers/shorten_dotted_string.ts deleted file mode 100644 index 9d78a96784339..0000000000000 --- a/src/plugins/discover/public/application/helpers/shorten_dotted_string.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const DOT_PREFIX_RE = /(.).+?\./g; - -/** - * Convert a dot.notated.string into a short - * version (d.n.string) - */ -export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index e1359eafe1c67..4b63eb5c56fd1 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -19,7 +19,14 @@ import React, { PureComponent } from 'react'; -import { EuiIcon, EuiInMemoryTable, EuiIconTip, EuiBasicTableColumn } from '@elastic/eui'; +import { + EuiIcon, + EuiInMemoryTable, + EuiIconTip, + EuiBasicTableColumn, + EuiBadge, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -144,6 +151,11 @@ const editDescription = i18n.translate( { defaultMessage: 'Edit' } ); +const customNameDescription = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.customNameTooltip', + { defaultMessage: 'A custom name for the field.' } +); + interface IndexedFieldProps { indexPattern: IIndexPattern; items: IndexedFieldItem[]; @@ -160,7 +172,7 @@ export class Table extends PureComponent { return ( - {name} + {field.name} {field.info && field.info.length ? (   @@ -185,6 +197,15 @@ export class Table extends PureComponent { /> ) : null} + {field.customName && field.customName !== field.name ? ( +
+ + + {field.customName} + + +
+ ) : null}
); } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 23f0a83c591de..1a04aaf784839 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -47,10 +47,7 @@ const indexPattern = ({ } as unknown) as IndexPattern; const mockFieldToIndexPatternField = (spec: Record) => { - return new IndexPatternField( - (spec as unknown) as IndexPatternField['spec'], - spec.displayName as string - ); + return new IndexPatternField((spec as unknown) as IndexPatternField['spec']); }; const fields = [ diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 92f0c4576e931..e097271248bbd 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -89,7 +89,11 @@ export class IndexedFieldsTable extends Component< (fields, fieldFilter, indexedFieldTypeFilter) => { if (fieldFilter) { const normalizedFieldFilter = fieldFilter.toLowerCase(); - fields = fields.filter((field) => field.name.toLowerCase().includes(normalizedFieldFilter)); + fields = fields.filter( + (field) => + field.name.toLowerCase().includes(normalizedFieldFilter) || + (field.displayName && field.displayName.toLowerCase().includes(normalizedFieldFilter)) + ); } if (indexedFieldTypeFilter) { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index 1e8fb6f9492fe..babfbbfc2a763 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -51,6 +51,22 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` value="" /> + + } + label="Custom name" + > + + - + + } + label="Custom name" + > + + - + + } + label="Custom name" + > + + - + + } + label="Custom name" + > + + @@ -954,7 +1021,7 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` isInvalid={false} label="Script" > - + + } + label="Custom name" + > + + - ({})); @@ -37,6 +38,7 @@ jest.mock('@elastic/eui', () => ({ EuiButtonEmpty: 'eui-button-empty', EuiCallOut: 'eui-call-out', EuiCode: 'eui-code', + EuiCodeEditor: 'eui-code-editor', EuiConfirmModal: 'eui-confirm-modal', EuiFieldNumber: 'eui-field-number', EuiFieldText: 'eui-field-text', @@ -173,6 +175,60 @@ describe('FieldEditor', () => { expect(component).toMatchSnapshot(); }); + it('should display and update a customName correctly', async () => { + let testField = ({ + name: 'test', + format: new Format(), + lang: undefined, + type: 'string', + customName: 'Test', + } as unknown) as IndexPatternField; + fieldList.push(testField); + indexPattern.fields.getByName = (name) => { + const flds = { + [testField.name]: testField, + }; + return flds[name]; + }; + indexPattern.fields = { + ...indexPattern.fields, + ...{ + update: (fld) => { + testField = (fld as unknown) as IndexPatternField; + }, + add: jest.fn(), + }, + }; + indexPattern.fieldFormatMap = { test: field }; + indexPattern.deleteFieldFormat = jest.fn(); + + const component = createComponentWithContext( + FieldEditor, + { + indexPattern, + spec: (testField as unknown) as IndexPatternField, + services: { + redirectAway: () => {}, + indexPatternService: ({ + updateSavedObject: jest.fn(() => Promise.resolve()), + } as unknown) as IndexPatternsService, + }, + }, + mockContext + ); + + await new Promise((resolve) => process.nextTick(resolve)); + component.update(); + const input = findTestSubject(component, 'editorFieldCustomName'); + expect(input.props().value).toBe('Test'); + input.simulate('change', { target: { value: 'new Test' } }); + const saveBtn = findTestSubject(component, 'fieldSaveButton'); + + await saveBtn.simulate('click'); + await new Promise((resolve) => process.nextTick(resolve)); + expect(testField.customName).toEqual('new Test'); + }); + it('should show deprecated lang warning', async () => { const testField = { ...field, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index d02338a6aee24..97d30d88e018c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -126,6 +126,7 @@ export interface FieldEditorState { errors?: string[]; format: any; spec: IndexPatternField['spec']; + customName: string; } export interface FieldEdiorProps { @@ -166,6 +167,7 @@ export class FieldEditor extends PureComponent + } + > + { + this.setState({ customName: e.target.value }); + }} + /> + + ); + } + /** * renders a warning and a table of conflicting indices * in case there are indices with different types @@ -772,7 +802,7 @@ export class FieldEditor extends PureComponent { const field = this.state.spec; const { indexPattern } = this.props; - const { fieldFormatId, fieldFormatParams } = this.state; + const { fieldFormatId, fieldFormatParams, customName } = this.state; if (field.scripted) { this.setState({ @@ -813,6 +843,11 @@ export class FieldEditor extends PureComponent { @@ -873,6 +908,7 @@ export class FieldEditor extends PureComponent {this.renderScriptingPanels()} {this.renderName()} + {this.renderCustomName()} {this.renderLanguage()} {this.renderType()} {this.renderTypeConflict()} diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 4f509876a75f4..75fd2c30a995a 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -57,7 +57,7 @@ export interface TableListViewProps { listingLimit: number; initialFilter: string; initialPageSize: number; - noItemsFragment: JSX.Element; + noItemsFragment?: JSX.Element; tableColumns: Array>; tableListTitle: string; toastNotifications: ToastsStart; @@ -73,7 +73,7 @@ export interface TableListViewProps { /** * Describes the content of the table. If not specified, the caption will be "This table contains {itemCount} rows." */ - tableCaption: string; + tableCaption?: string; searchFilters?: SearchFilterConfig[]; } @@ -445,6 +445,7 @@ class TableListView extends React.Component void; diff --git a/src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss b/src/plugins/vis_type_timelion/public/timelion_options.scss similarity index 77% rename from src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss rename to src/plugins/vis_type_timelion/public/timelion_options.scss index 3f274520cce63..7cd0c855653c8 100644 --- a/src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss +++ b/src/plugins/vis_type_timelion/public/timelion_options.scss @@ -26,3 +26,11 @@ max-height: $euiSize * 15; } } + +.visEditor--timelion { + .visEditorSidebar__timelionOptions { + flex: 1 1 auto; + display: flex; + flex-direction: column; + } +} diff --git a/src/plugins/vis_type_timelion/public/timelion_options.tsx b/src/plugins/vis_type_timelion/public/timelion_options.tsx index 1ef8088c7a714..90a39c2c90e26 100644 --- a/src/plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_options.tsx @@ -27,6 +27,8 @@ import { TimelionVisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; import { TimelionVisDependencies } from './plugin'; +import './timelion_options.scss'; + export type TimelionOptionsProps = VisOptionsProps; function TimelionOptions({ diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 3c9996ca44ff8..fe5c04c001731 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -293,6 +293,16 @@ export default function ({ getService, getPageObjects }) { const currentUrlWithoutScore = await browser.getCurrentUrl(); expect(currentUrlWithoutScore).not.to.contain('_score'); }); + it('should add a field with customLabel, sort by it, display it correctly', async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.clickFieldListItemAdd('referer'); + await PageObjects.discover.clickFieldSort('referer'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('Referer custom'); + expect(await PageObjects.discover.getAllFieldNames()).to.contain('Referer custom'); + const url = await browser.getCurrentUrl(); + expect(url).to.contain('referer'); + }); }); describe('refresh interval', function () { diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js index bd7511d373b90..5b0b7af56b332 100644 --- a/test/functional/apps/visualize/_data_table.js +++ b/test/functional/apps/visualize/_data_table.js @@ -209,6 +209,29 @@ export default function ({ getService, getPageObjects }) { ]); }); + it('should show correct data when selecting a field by its custom name', async () => { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickDataTable(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('UTC time'); + await PageObjects.visEditor.setInterval('Day'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', + '4,757', + '2015-09-21', + '4,614', + '2015-09-22', + '4,633', + ]); + const header = await PageObjects.visChart.getTableVisHeader(); + expect(header).to.contain('UTC time'); + }); + it('should correctly filter for applied time filter on the main timefield', async () => { await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); diff --git a/test/functional/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json index 9158a3023fc5e..0f9820a6c2f6e 100644 --- a/test/functional/fixtures/es_archiver/discover/data.json +++ b/test/functional/fixtures/es_archiver/discover/data.json @@ -7,7 +7,8 @@ "index-pattern": { "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", "timeFieldName": "@timestamp", - "title": "logstash-*" + "title": "logstash-*", + "fieldAttrs": "{\"referer\":{\"customName\":\"Referer custom\"}}" }, "type": "index-pattern" } diff --git a/test/functional/fixtures/es_archiver/discover/mappings.json b/test/functional/fixtures/es_archiver/discover/mappings.json index 82002c095bcc5..53bbe8a5baa5b 100644 --- a/test/functional/fixtures/es_archiver/discover/mappings.json +++ b/test/functional/fixtures/es_archiver/discover/mappings.json @@ -93,6 +93,9 @@ }, "title": { "type": "text" + }, + "fieldAttrs": { + "type": "text" } } }, diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index abca5a98bf7fd..c57cdb40ae952 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -8,7 +8,8 @@ "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "timeFieldName": "@timestamp", - "title": "logstash-*" + "title": "logstash-*", + "fieldAttrs": "{\"utc_time\":{\"customName\":\"UTC time\"}}" }, "type": "index-pattern" } diff --git a/test/functional/fixtures/es_archiver/visualize/mappings.json b/test/functional/fixtures/es_archiver/visualize/mappings.json index a50aed233eea6..464f6751eac5c 100644 --- a/test/functional/fixtures/es_archiver/visualize/mappings.json +++ b/test/functional/fixtures/es_archiver/visualize/mappings.json @@ -93,6 +93,9 @@ }, "title": { "type": "text" + }, + "fieldAttrs": { + "type": "text" } } }, diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 21f0991f7efde..265f47773dd45 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -126,9 +126,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide */ public async onDashboardLandingPage() { log.debug(`onDashboardLandingPage`); - return await testSubjects.exists('dashboardLandingPage', { - timeout: 5000, - }); + return await listingTable.onListingPage('dashboard'); } public async expectExistsDashboardLandingPage() { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 2423f66a4b34e..9c5bedf7c242d 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -246,9 +246,9 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider public async getAllFieldNames() { const sidebar = await testSubjects.find('discover-sidebar'); const $ = await sidebar.parseDomContent(); - return $('.dscSidebar__item[attr-field]') + return $('.dscSidebarField__name') .toArray() - .map((field) => $(field).find('span.eui-textTruncate').text()); + .map((field) => $(field).text()); } public async getSidebarWidth() { diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 1acea624ad4cd..3e3f60ca17131 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -328,6 +328,13 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr return await testSubjects.getVisibleText('paginated-table-body'); } + /** + * This function returns the text displayed in the Table Vis header + */ + public async getTableVisHeader() { + return await testSubjects.getVisibleText('paginated-table-header'); + } + /** * This function is the newer function to retrieve data from within a table visualization. * It uses a better return format, than the old getTableVisData, by properly splitting diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index 0778fe954879d..53b45697136ed 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -20,21 +20,19 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; +type AppName = 'visualize' | 'dashboard' | 'map'; + export function ListingTableProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const log = getService('log'); const retry = getService('retry'); const { common, header } = getPageObjects(['common', 'header']); - const prefixMap = { visualize: 'vis', dashboard: 'dashboard' }; + const prefixMap = { visualize: 'vis', dashboard: 'dashboard', map: 'map' }; - /** - * This class provides functions for dashboard and visualize landing pages - */ class ListingTable { private async getSearchFilter() { - const searchFilter = await find.allByCssSelector('main .euiFieldSearch'); - return searchFilter[0]; + return await testSubjects.find('tableListSearchBox'); } /** @@ -86,9 +84,8 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider /** * Returns items count on landing page - * @param appName 'visualize' | 'dashboard' */ - public async expectItemsCount(appName: 'visualize' | 'dashboard', count: number) { + public async expectItemsCount(appName: AppName, count: number) { await retry.try(async () => { const elements = await find.allByCssSelector( `[data-test-subj^="${prefixMap[appName]}ListingTitleLink"]` @@ -126,14 +123,8 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider /** * Searches for item on Landing page and retruns items count that match `ListingTitleLink-${name}` pattern - * @param appName 'visualize' | 'dashboard' - * @param name item name */ - public async searchAndExpectItemsCount( - appName: 'visualize' | 'dashboard', - name: string, - count: number - ) { + public async searchAndExpectItemsCount(appName: AppName, name: string, count: number) { await this.searchForItemWithName(name); await retry.try(async () => { const links = await testSubjects.findAll( @@ -165,10 +156,8 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider /** * Clicks item on Landing page by link name if it is present - * @param appName 'dashboard' | 'visualize' - * @param name item name */ - public async clickItemLink(appName: 'dashboard' | 'visualize', name: string) { + public async clickItemLink(appName: AppName, name: string) { await testSubjects.click( `${prefixMap[appName]}ListingTitleLink-${name.split(' ').join('-')}` ); @@ -204,6 +193,12 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider } }); } + + public async onListingPage(appName: AppName) { + return await testSubjects.exists(`${appName}LandingPage`, { + timeout: 5000, + }); + } } return new ListingTable(); diff --git a/test/server_integration/http/platform/headers.ts b/test/server_integration/http/platform/headers.ts index 260bc37bd1328..50cfa5c702231 100644 --- a/test/server_integration/http/platform/headers.ts +++ b/test/server_integration/http/platform/headers.ts @@ -31,7 +31,7 @@ export default function ({ getService }: FtrProviderContext) { const config = getService('config'); describe('headers timeout ', () => { - it('issue-73849', async () => { + it('handles correctly. See issue #73849', async () => { const agent = new Http.Agent({ keepAlive: true, }); @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { } await performRequest(); - const defaultHeadersTimeout = 40 * oneSec; + const defaultHeadersTimeout = 60 * oneSec; await delay(defaultHeadersTimeout + oneSec); await performRequest(); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index 14bddceb1c03d..e97b37f16faf0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -31,7 +31,6 @@ import { } from '../types'; import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib'; import { - InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../security/server'; @@ -48,6 +47,7 @@ import { IEvent } from '../../../event_log/server'; import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; +import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -72,7 +72,6 @@ export interface ConstructorOptions { namespace?: string; getUserName: () => Promise; createAPIKey: (name: string) => Promise; - invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; getActionsClient: () => Promise; getEventLogClient: () => Promise; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; @@ -172,9 +171,6 @@ export class AlertsClient { private readonly authorization: AlertsAuthorization; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: (name: string) => Promise; - private readonly invalidateAPIKey: ( - params: InvalidateAPIKeyParams - ) => Promise; private readonly getActionsClient: () => Promise; private readonly actionsAuthorization: ActionsAuthorization; private readonly getEventLogClient: () => Promise; @@ -191,7 +187,6 @@ export class AlertsClient { namespace, getUserName, createAPIKey, - invalidateAPIKey, encryptedSavedObjectsClient, getActionsClient, actionsAuthorization, @@ -207,7 +202,6 @@ export class AlertsClient { this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; this.authorization = authorization; this.createAPIKey = createAPIKey; - this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; this.getActionsClient = getActionsClient; this.actionsAuthorization = actionsAuthorization; @@ -263,7 +257,11 @@ export class AlertsClient { ); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: rawAlert.apiKey }); + markApiKeyForInvalidation( + { apiKey: rawAlert.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } if (data.enabled) { @@ -487,7 +485,13 @@ export class AlertsClient { await Promise.all([ taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, - apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, + apiKeyToInvalidate + ? markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, ]); return removeResult; @@ -526,7 +530,11 @@ export class AlertsClient { await Promise.all([ alertSavedObject.attributes.apiKey - ? this.invalidateApiKey({ apiKey: alertSavedObject.attributes.apiKey }) + ? markApiKeyForInvalidation( + { apiKey: alertSavedObject.attributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ) : null, (async () => { if ( @@ -591,7 +599,11 @@ export class AlertsClient { ); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: createAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: createAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } @@ -671,28 +683,20 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: updateAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } if (apiKeyToInvalidate) { - await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); - } - } - - private async invalidateApiKey({ apiKey }: { apiKey: string | null }): Promise { - if (!apiKey) { - return; - } - - try { - const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; - const response = await this.invalidateAPIKey({ id: apiKeyId }); - if (response.apiKeysEnabled === true && response.result.error_count > 0) { - this.logger.error(`Failed to invalidate API Key [id="${apiKeyId}"]`); - } - } catch (e) { - this.logger.error(`Failed to invalidate API Key: ${e.message}`); + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); } } @@ -752,7 +756,11 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: updateAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } const scheduledTask = await this.scheduleAlert( @@ -764,7 +772,11 @@ export class AlertsClient { scheduledTaskId: scheduledTask.id, }); if (apiKeyToInvalidate) { - await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); } } } @@ -825,7 +837,13 @@ export class AlertsClient { attributes.scheduledTaskId ? deleteTaskIfItExists(this.taskManager, attributes.scheduledTaskId) : null, - apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, + apiKeyToInvalidate + ? await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, ]); } } diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts index 0f89fc6c9c25c..cc5d10c3346e8 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index 965ea1949bf3a..ee407b1a6d50c 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -711,7 +710,7 @@ describe('create()', () => { expect(taskManager.schedule).not.toHaveBeenCalled(); }); - test('throws error and invalidates API key when create saved object fails', async () => { + test('throws error and add API key to invalidatePendingApiKey SO when create saved object fails', async () => { const data = getMockData(); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -731,11 +730,25 @@ describe('create()', () => { ], }); unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + const createdAt = new Date().toISOString(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` ); expect(taskManager.schedule).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); + expect(unsecuredSavedObjectsClient.create.mock.calls[1][1]).toStrictEqual({ + apiKeyId: '123', + createdAt, + }); }); test('attempts to remove saved object if scheduling failed', async () => { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts index d9b253c3a56e8..e7b975aec8eb0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts @@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -94,11 +93,22 @@ describe('delete()', () => { }); test('successfully removes an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); @@ -107,12 +117,21 @@ describe('delete()', () => { test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' @@ -133,6 +152,15 @@ describe('delete()', () => { }); test(`doesn't invalidate API key when apiKey is null`, async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ ...existingAlert, attributes: { @@ -142,24 +170,34 @@ describe('delete()', () => { }); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); test('swallows error when getDecryptedAsInternalUser throws an error', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts index d0557df622028..11ce0027f82d8 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -33,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -108,6 +108,15 @@ describe('disable()', () => { }); test('disables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -145,11 +154,22 @@ describe('disable()', () => { } ); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); }); test('falls back when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); @@ -188,7 +208,7 @@ describe('disable()', () => { } ); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test(`doesn't disable already disabled alerts`, async () => { @@ -201,26 +221,54 @@ describe('disable()', () => { }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.remove).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test(`doesn't invalidate when no API key is used`, async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); await alertsClient.disable({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when failing to load decrypted saved object', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(taskManager.remove).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'disable(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -235,11 +283,10 @@ describe('disable()', () => { }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts index 215493c71aec7..16e83c42d8930 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts @@ -14,6 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -34,7 +35,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -147,6 +147,7 @@ describe('enable()', () => { }); test('enables an alert', async () => { + const createdAt = new Date().toISOString(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ ...existingAlert, attributes: { @@ -157,13 +158,22 @@ describe('enable()', () => { updatedBy: 'elastic', }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', @@ -217,6 +227,7 @@ describe('enable()', () => { }); test('invalidates API key if ever one existed prior to updating', async () => { + const createdAt = new Date().toISOString(); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ ...existingAlert, attributes: { @@ -224,13 +235,24 @@ describe('enable()', () => { apiKey: Buffer.from('123:abc').toString('base64'), }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); }); test(`doesn't enable already enabled alerts`, async () => { @@ -312,19 +334,31 @@ describe('enable()', () => { }); test('throws error when failing to update the first time', async () => { + const createdAt = new Date().toISOString(); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update"` ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); expect(taskManager.schedule).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts index c1adaddc80d9e..1b3a776bd23e0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -35,7 +35,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts index 004230403de2e..5c0d80f159b31 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 9cb2a33222d23..269b2eb2ab7a7 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -39,7 +39,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts index 8b32f05f6d5a1..79a064beba166 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts index 5ebb4e90d4b50..028a7c6737474 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts @@ -46,14 +46,6 @@ export function getBeforeSetup( ) { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); const actionsClient = actionsClientMock.create(); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts index b2f5c5498f848..8cbe47655ef68 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts index 88199dfd1f7b9..868fa3d8c6aa2 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts @@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts index cd7112b3551b3..05ca741f480ca 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts index 07666c1cc6261..5ef1af9b6f0ee 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts index 97711b8c14579..88692239ac2fe 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index 1dcde6addb9bf..ad58e36ade722 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -10,7 +10,7 @@ import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; -import { IntervalSchedule } from '../../types'; +import { IntervalSchedule, InvalidatePendingApiKey } from '../../types'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; @@ -38,7 +38,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -161,6 +160,15 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -241,7 +249,7 @@ describe('update()', () => { namespace: 'default', }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` @@ -376,6 +384,24 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -423,7 +449,7 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` @@ -530,6 +556,15 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -578,7 +613,7 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` @@ -732,7 +767,6 @@ describe('update()', () => { }); it('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { @@ -775,6 +809,7 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); // add ApiKey to invalidate await alertsClient.update({ id: '1', data: { @@ -797,7 +832,7 @@ describe('update()', () => { }, }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); @@ -965,8 +1000,9 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[1][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('234'); }); describe('updating an alert schedule', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts index 1f3b567b2c031..af178a1fac5f5 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts @@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -32,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -80,6 +80,15 @@ describe('updateApiKey()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -121,11 +130,22 @@ describe('updateApiKey()', () => { }, { version: '123' } ); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); }); test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.updateApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); @@ -160,28 +180,37 @@ describe('updateApiKey()', () => { }, { version: '123' } ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.updateApiKey({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); }); test('swallows error when getting decrypted object throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.updateApiKey({ id: '1' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' ); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { @@ -190,12 +219,22 @@ describe('updateApiKey()', () => { result: { id: '234', name: '234', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail"` ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('234'); }); describe('authorization', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts index b1ac5ac4c6783..ca9389ece310c 100644 --- a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts @@ -45,7 +45,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger, encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -115,7 +114,7 @@ async function update(success: boolean) { ); return expectConflict(success, err, 'create'); } - expectSuccess(success, 2, 'create'); + expectSuccess(success, 3, 'create'); // only checking the debug messages in this test expect(logger.debug).nthCalledWith(1, `alertsClient.update('alert-id') conflict, retrying ...`); @@ -306,14 +305,6 @@ beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 3cf6666e90eb0..bdbfc726dab8f 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -92,7 +92,7 @@ test('creates an alerts client with proper constructor arguments when security i expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); @@ -125,7 +125,6 @@ test('creates an alerts client with proper constructor arguments when security i getActionsClient: expect.any(Function), getEventLogClient: expect.any(Function), createAPIKey: expect.any(Function), - invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, kibanaVersion: '7.10.0', }); @@ -142,7 +141,7 @@ test('creates an alerts client with proper constructor arguments', async () => { expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); @@ -167,7 +166,6 @@ test('creates an alerts client with proper constructor arguments', async () => { namespace: 'default', getUserName: expect.any(Function), createAPIKey: expect.any(Function), - invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, getActionsClient: expect.any(Function), getEventLogClient: expect.any(Function), diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index eccd810391307..069703be72f8a 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -14,7 +14,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions import { AlertsClient } from './alerts_client'; import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; @@ -94,7 +94,7 @@ export class AlertsClientFactory { alertTypeRegistry: this.alertTypeRegistry, unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), authorization, actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), @@ -129,22 +129,6 @@ export class AlertsClientFactory { result: createAPIKeyResult, }; }, - async invalidateAPIKey(params: InvalidateAPIKeyParams) { - if (!securityPluginSetup) { - return { apiKeysEnabled: false }; - } - const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( - params - ); - // Null when Elasticsearch security is disabled - if (!invalidateAPIKeyResult) { - return { apiKeysEnabled: false }; - } - return { - apiKeysEnabled: true, - result: invalidateAPIKeyResult, - }; - }, async getActionsClient() { return actions.getActionsClientWithRequest(request); }, diff --git a/x-pack/plugins/alerts/server/config.test.ts b/x-pack/plugins/alerts/server/config.test.ts index 93aa3c38a0460..bf3b30b5d2378 100644 --- a/x-pack/plugins/alerts/server/config.test.ts +++ b/x-pack/plugins/alerts/server/config.test.ts @@ -13,6 +13,10 @@ describe('config validation', () => { "healthCheck": Object { "interval": "60m", }, + "invalidateApiKeysTask": Object { + "interval": "5m", + "removalDelay": "5m", + }, } `); }); diff --git a/x-pack/plugins/alerts/server/config.ts b/x-pack/plugins/alerts/server/config.ts index a6d2196a407b5..41340c7dfe5fc 100644 --- a/x-pack/plugins/alerts/server/config.ts +++ b/x-pack/plugins/alerts/server/config.ts @@ -11,6 +11,10 @@ export const configSchema = schema.object({ healthCheck: schema.object({ interval: schema.string({ validate: validateDurationSchema, defaultValue: '60m' }), }), + invalidateApiKeysTask: schema.object({ + interval: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + }), }); export type AlertsConfig = TypeOf; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts new file mode 100644 index 0000000000000..7b30c22c47f8a --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loggingSystemMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { markApiKeyForInvalidation } from './mark_api_key_for_invalidation'; + +describe('markApiKeyForInvalidation', () => { + test('should call savedObjectsClient create with the proper params', async () => { + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + await markApiKeyForInvalidation( + { apiKey: Buffer.from('123:abc').toString('base64') }, + loggingSystemMock.create().get(), + unsecuredSavedObjectsClient + ); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(2); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual( + 'api_key_pending_invalidation' + ); + }); + + test('should log the proper error when savedObjectsClient create failed', async () => { + const logger = loggingSystemMock.create().get(); + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); + await markApiKeyForInvalidation( + { apiKey: Buffer.from('123').toString('base64') }, + logger, + unsecuredSavedObjectsClient + ); + expect(logger.error).toHaveBeenCalledWith( + 'Failed to mark for API key [id="MTIz"] for invalidation: Fail' + ); + }); +}); diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts new file mode 100644 index 0000000000000..db25f5b3e19eb --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger, SavedObjectsClientContract } from 'src/core/server'; + +export const markApiKeyForInvalidation = async ( + { apiKey }: { apiKey: string | null }, + logger: Logger, + savedObjectsClient: SavedObjectsClientContract +): Promise => { + if (!apiKey) { + return; + } + try { + const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; + await savedObjectsClient.create('api_key_pending_invalidation', { + apiKeyId, + createdAt: new Date().toISOString(), + }); + } catch (e) { + logger.error(`Failed to mark for API key [id="${apiKey}"] for invalidation: ${e.message}`); + } +}; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts new file mode 100644 index 0000000000000..77cbb9f4a4a85 --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Logger, + CoreStart, + SavedObjectsFindResponse, + KibanaRequest, + SavedObjectsClientContract, +} from 'kibana/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../security/server'; +import { + RunContext, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../task_manager/server'; +import { InvalidateAPIKeyResult } from '../alerts_client'; +import { AlertsConfig } from '../config'; +import { timePeriodBeforeDate } from '../lib/get_cadence'; +import { AlertingPluginsStart } from '../plugin'; +import { InvalidatePendingApiKey } from '../types'; + +const TASK_TYPE = 'alerts_invalidate_api_keys'; +export const TASK_ID = `Alerts-${TASK_TYPE}`; + +const invalidateAPIKey = async ( + params: InvalidateAPIKeyParams, + securityPluginSetup?: SecurityPluginSetup +): Promise => { + if (!securityPluginSetup) { + return { apiKeysEnabled: false }; + } + const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( + params + ); + // Null when Elasticsearch security is disabled + if (!invalidateAPIKeyResult) { + return { apiKeysEnabled: false }; + } + return { + apiKeysEnabled: true, + result: invalidateAPIKeyResult, + }; +}; + +export function initializeApiKeyInvalidator( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + taskManager: TaskManagerSetupContract, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + registerApiKeyInvalitorTaskDefinition( + logger, + coreStartServices, + taskManager, + config, + securityPluginSetup + ); +} + +export async function scheduleApiKeyInvalidatorTask( + logger: Logger, + config: Promise, + taskManager: TaskManagerStartContract +) { + const interval = (await config).invalidateApiKeysTask.interval; + try { + await taskManager.ensureScheduled({ + id: TASK_ID, + taskType: TASK_TYPE, + schedule: { + interval, + }, + state: {}, + params: {}, + }); + } catch (e) { + logger.debug(`Error scheduling task, received ${e.message}`); + } +} + +function registerApiKeyInvalitorTaskDefinition( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + taskManager: TaskManagerSetupContract, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + taskManager.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Invalidate alert API Keys', + createTaskRunner: taskRunner(logger, coreStartServices, config, securityPluginSetup), + }, + }); +} + +function getFakeKibanaRequest(basePath: string) { + const requestHeaders: Record = {}; + return ({ + headers: requestHeaders, + getBasePath: () => basePath, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown) as KibanaRequest; +} + +function taskRunner( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + return ({ taskInstance }: RunContext) => { + const { state } = taskInstance; + return { + async run() { + let totalInvalidated = 0; + const configResult = await config; + try { + const [{ savedObjects, http }, { encryptedSavedObjects }] = await coreStartServices; + const savedObjectsClient = savedObjects.getScopedClient( + getFakeKibanaRequest(http.basePath.serverBasePath), + { + includedHiddenTypes: ['api_key_pending_invalidation'], + excludedWrappers: ['security'], + } + ); + const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ + includedHiddenTypes: ['api_key_pending_invalidation'], + }); + const configuredDelay = configResult.invalidateApiKeysTask.removalDelay; + const delay = timePeriodBeforeDate(new Date(), configuredDelay).toISOString(); + + let hasApiKeysPendingInvalidation = true; + const PAGE_SIZE = 100; + do { + const apiKeysToInvalidate = await savedObjectsClient.find({ + type: 'api_key_pending_invalidation', + filter: `api_key_pending_invalidation.attributes.createdAt <= "${delay}"`, + page: 1, + sortField: 'createdAt', + sortOrder: 'asc', + perPage: PAGE_SIZE, + }); + totalInvalidated += await invalidateApiKeys( + logger, + savedObjectsClient, + apiKeysToInvalidate, + encryptedSavedObjectsClient, + securityPluginSetup + ); + + hasApiKeysPendingInvalidation = apiKeysToInvalidate.total > PAGE_SIZE; + } while (hasApiKeysPendingInvalidation); + + return { + state: { + runs: (state.runs || 0) + 1, + total_invalidated: totalInvalidated, + }, + schedule: { + interval: configResult.invalidateApiKeysTask.interval, + }, + }; + } catch (e) { + logger.warn(`Error executing alerting apiKey invalidation task: ${e.message}`); + return { + state: { + runs: (state.runs || 0) + 1, + total_invalidated: totalInvalidated, + }, + schedule: { + interval: configResult.invalidateApiKeysTask.interval, + }, + }; + } + }, + }; + }; +} + +async function invalidateApiKeys( + logger: Logger, + savedObjectsClient: SavedObjectsClientContract, + apiKeysToInvalidate: SavedObjectsFindResponse, + encryptedSavedObjectsClient: EncryptedSavedObjectsClient, + securityPluginSetup?: SecurityPluginSetup +) { + let totalInvalidated = 0; + await Promise.all( + apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { + const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser< + InvalidatePendingApiKey + >('api_key_pending_invalidation', apiKeyObj.id); + const apiKeyId = decryptedApiKey.attributes.apiKeyId; + const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup); + if (response.apiKeysEnabled === true && response.result.error_count > 0) { + logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`); + } else { + try { + await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id); + totalInvalidated++; + } catch (err) { + logger.error( + `Failed to cleanup api key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}` + ); + } + } + }) + ); + logger.debug(`Total invalidated api keys "${totalInvalidated}"`); + return totalInvalidated; +} diff --git a/x-pack/plugins/alerts/server/lib/get_cadence.ts b/x-pack/plugins/alerts/server/lib/get_cadence.ts new file mode 100644 index 0000000000000..d09ed0c2122cd --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/get_cadence.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { memoize } from 'lodash'; + +export enum TimeUnit { + Minute = 'm', + Second = 's', + Hour = 'h', + Day = 'd', +} +const VALID_CADENCE = new Set(Object.values(TimeUnit)); +const CADENCE_IN_MS: Record = { + [TimeUnit.Second]: 1000, + [TimeUnit.Minute]: 60 * 1000, + [TimeUnit.Hour]: 60 * 60 * 1000, + [TimeUnit.Day]: 24 * 60 * 60 * 1000, +}; + +const isNumeric = (numAsStr: string) => /^\d+$/.test(numAsStr); + +export const parseIntervalAsMillisecond = memoize((value: string): number => { + const numericAsStr: string = value.slice(0, -1); + const numeric: number = parseInt(numericAsStr, 10); + const cadence: TimeUnit | string = value.slice(-1); + if ( + !VALID_CADENCE.has(cadence as TimeUnit) || + isNaN(numeric) || + numeric <= 0 || + !isNumeric(numericAsStr) + ) { + throw new Error( + `Invalid time value "${value}". Time must be of the form {number}m. Example: 5m.` + ); + } + return numeric * CADENCE_IN_MS[cadence as TimeUnit]; +}); + +/** + * Returns a date that is the specified interval from given date. + * + * @param {Date} date - The date to add interval to + * @param {string} interval - THe time of the form `Nm` such as `5m` + */ +export function timePeriodBeforeDate(date: Date, timePeriod: string): Date { + const result = new Date(date.valueOf()); + const milisecFromTime = parseIntervalAsMillisecond(timePeriod); + result.setMilliseconds(result.getMilliseconds() - milisecFromTime); + return result; +} diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 715fbc6aeed45..62f4b7d5a3fc4 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -22,6 +22,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); @@ -67,6 +71,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); @@ -114,6 +122,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 1fa89606a76fc..0c91e93938346 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -65,6 +65,10 @@ import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/ import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; import { setupSavedObjects } from './saved_objects'; +import { + initializeApiKeyInvalidator, + scheduleApiKeyInvalidatorTask, +} from './invalidate_pending_api_keys/task'; import { getHealthStatusStream, scheduleAlertingHealthCheck, @@ -200,6 +204,14 @@ export class AlertingPlugin { }); } + initializeApiKeyInvalidator( + this.logger, + core.getStartServices(), + plugins.taskManager, + this.config, + this.security + ); + core.getStartServices().then(async ([, startPlugins]) => { core.status.set( combineLatest([ @@ -308,7 +320,9 @@ export class AlertingPlugin { }); scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager); + scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager); + scheduleApiKeyInvalidatorTask(this.telemetryLogger, this.config, plugins.taskManager); return { listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!), diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index 9aa1f86676eaa..da30273e93c6b 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -42,10 +42,32 @@ export function setupSavedObjects( mappings: mappings.alert, }); + savedObjects.registerType({ + name: 'api_key_pending_invalidation', + hidden: true, + namespaceType: 'agnostic', + mappings: { + properties: { + apiKeyId: { + type: 'keyword', + }, + createdAt: { + type: 'date', + }, + }, + }, + }); + // Encrypted attributes encryptedSavedObjects.registerType({ type: 'alert', attributesToEncrypt: new Set(['apiKey']), attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD), }); + + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'api_key_pending_invalidation', + attributesToEncrypt: new Set(['apiKeyId']), + }); } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 9226461f6e30a..dde1628156658 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -180,4 +180,16 @@ export interface AlertsConfigType { }; } +export interface AlertsConfigType { + invalidateApiKeysTask: { + interval: string; + removalDelay: string; + }; +} + +export interface InvalidatePendingApiKey { + apiKeyId: string; + createdAt: string; +} + export type AlertTypeRegistry = PublicMethodsOf; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 44bd7d6c73d8e..50667d3135f1a 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -13,7 +13,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; @@ -24,16 +23,11 @@ import { SearchBar } from '../../shared/search_bar'; import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; import { TableLinkFlexItem } from './table_link_flex_item'; -const rowHeight = 310; -const latencyChartRowHeight = 230; - -const Row = styled(EuiFlexItem)` - height: ${rowHeight}px; -`; - -const LatencyChartRow = styled(EuiFlexItem)` - height: ${latencyChartRowHeight}px; -`; +/** + * The height a chart should be if it's next to a table with 5 rows and a title. + * Add the height of the pagination row. + */ +export const chartHeight = 322; interface ServiceOverviewProps { agentName?: string; @@ -52,7 +46,7 @@ export function ServiceOverview({ - +

@@ -65,8 +59,8 @@ export function ServiceOverview({

-
- + + @@ -111,12 +105,15 @@ export function ServiceOverview({ - - + + {!isRumAgentName(agentName) && ( - + )} @@ -125,8 +122,8 @@ export function ServiceOverview({ - - + + @@ -175,8 +172,8 @@ export function ServiceOverview({ - - + + @@ -207,7 +204,7 @@ export function ServiceOverview({ - +
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx index 4c8d368811a0c..912490d866e88 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx @@ -4,27 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; export function FetchWrapper({ - hasData, status, children, }: { - hasData: boolean; status: FETCH_STATUS; - children: React.ReactNode; + children: ReactNode; }) { if (status === FETCH_STATUS.FAILURE) { return ; } - if (!hasData && status !== FETCH_STATUS.SUCCESS) { - return ; - } - return <>{children}; } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index a5a002cf3aca4..34b934c41cca3 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -3,25 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; -import { EuiTitle } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; +import { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable } from '@elastic/eui'; -import { EuiBasicTableColumn } from '@elastic/eui'; +import React, { useState } from 'react'; import styled from 'styled-components'; -import { EuiToolTip } from '@elastic/eui'; import { asInteger } from '../../../../../common/utils/formatters'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; -import { TableLinkFlexItem } from '../table_link_flex_item'; -import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { px, truncate, unit } from '../../../../style/variables'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ServiceOverviewTable } from '../service_overview_table'; +import { TableLinkFlexItem } from '../table_link_flex_item'; import { FetchWrapper } from './fetch_wrapper'; interface Props { @@ -108,7 +110,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { render: (_, { last_seen: lastSeen }) => { return ; }, - width: px(unit * 8), + width: px(unit * 9), }, { field: 'occurrences', @@ -223,8 +225,8 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { - - + ` + height: ${tableHeight}px; + display: flex; + flex-direction: column; + + .euiBasicTable { + display: flex; + flex-direction: column; + flex-grow: 1; + + > :first-child { + flex-grow: 1; + } + } + + .euiTableRowCell { + visibility: ${({ isEmptyAndLoading }) => + isEmptyAndLoading ? 'hidden' : 'visible'}; + } +`; + +export function ServiceOverviewTable(props: EuiBasicTableProps) { + const { items, loading } = props; + const isEmptyAndLoading = !!(items.length === 0 && loading); + + return ( + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index 30d4bb34ea345..c453de709a5d2 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` + `"/app/ml/jobs?_a=(jobs:(queryText:'id:(something)%20groups:(apm)'))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx index 507acc49d89db..b40df89a22c33 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx @@ -31,6 +31,7 @@ import { onBrushEnd } from '../helper/helper'; interface Props { id: string; fetchStatus: FETCH_STATUS; + height?: number; onToggleLegend?: LegendItemListener; timeseries: TimeSeries[]; /** @@ -44,10 +45,9 @@ interface Props { showAnnotations?: boolean; } -const XY_HEIGHT = unit * 16; - export function LineChart({ id, + height = unit * 16, fetchStatus, onToggleLegend, timeseries, @@ -88,7 +88,7 @@ export function LineChart({ ); return ( - + onBrushEnd({ x, history })} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 2743d12a3eb04..5b977b6991612 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -27,10 +27,14 @@ function yTickFormat(y?: number | null) { } interface Props { + height?: number; showAnnotations?: boolean; } -export function TransactionErrorRateChart({ showAnnotations = true }: Props) { +export function TransactionErrorRateChart({ + height, + showAnnotations = true, +}: Props) { const theme = useTheme(); const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); @@ -71,6 +75,7 @@ export function TransactionErrorRateChart({ showAnnotations = true }: Props) { ({ + HttpLogic: { values: mockHttpValues }, +})); +const { http } = mockHttpValues; + +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'engine1' } }, +})); + +jest.mock('../../../shared/flash_messages', () => ({ + setQueuedSuccessMessage: jest.fn(), + flashAPIErrors: jest.fn(), +})); +import { setQueuedSuccessMessage, flashAPIErrors } from '../../../shared/flash_messages'; + +import { DocumentDetailLogic } from './document_detail_logic'; + +describe('DocumentDetailLogic', () => { + const DEFAULT_VALUES = { + dataLoading: true, + fields: [], + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + document_detail_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + DocumentDetailLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('actions', () => { + describe('setFields', () => { + it('should set fields to the provided value and dataLoading to false', () => { + const fields = [{ name: 'foo', value: ['foo'], type: 'string' }]; + + mount({ + dataLoading: true, + fields: [], + }); + + DocumentDetailLogic.actions.setFields(fields); + + expect(DocumentDetailLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + fields, + }); + }); + }); + + describe('getDocumentDetails', () => { + it('will call an API endpoint and then store the result', async () => { + const fields = [{ name: 'name', value: 'python', type: 'string' }]; + jest.spyOn(DocumentDetailLogic.actions, 'setFields'); + const promise = Promise.resolve({ fields }); + http.get.mockReturnValue(promise); + + DocumentDetailLogic.actions.getDocumentDetails('1'); + + expect(http.get).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); + await promise; + expect(DocumentDetailLogic.actions.setFields).toHaveBeenCalledWith(fields); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occurred'); + http.get.mockReturnValue(promise); + + try { + DocumentDetailLogic.actions.getDocumentDetails('1'); + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); + }); + }); + + describe('deleteDocument', () => { + let confirmSpy: any; + let promise: Promise; + + beforeEach(() => { + confirmSpy = jest.spyOn(window, 'confirm'); + confirmSpy.mockImplementation(jest.fn(() => true)); + promise = Promise.resolve({}); + http.delete.mockReturnValue(promise); + }); + + afterEach(() => { + confirmSpy.mockRestore(); + }); + + it('will call an API endpoint and show a success message', async () => { + mount(); + DocumentDetailLogic.actions.deleteDocument('1'); + + expect(http.delete).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); + await promise; + expect(setQueuedSuccessMessage).toHaveBeenCalledWith( + 'Successfully marked document for deletion. It will be deleted momentarily.' + ); + }); + + it('will do nothing if not confirmed', async () => { + mount(); + window.confirm = () => false; + + DocumentDetailLogic.actions.deleteDocument('1'); + + expect(http.delete).not.toHaveBeenCalled(); + await promise; + }); + + it('handles errors', async () => { + mount(); + promise = Promise.reject('An error occured'); + http.delete.mockReturnValue(promise); + + try { + DocumentDetailLogic.actions.deleteDocument('1'); + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts new file mode 100644 index 0000000000000..87bf149fb1680 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; +import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { FieldDetails } from './types'; + +interface DocumentDetailLogicValues { + dataLoading: boolean; + fields: FieldDetails[]; +} + +interface DocumentDetailLogicActions { + setFields(fields: FieldDetails[]): { fields: FieldDetails[] }; + deleteDocument(documentId: string): { documentId: string }; + getDocumentDetails(documentId: string): { documentId: string }; +} + +type DocumentDetailLogicType = MakeLogicType; + +const CONFIRM_DELETE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentDetail.confirmDelete', + { + defaultMessage: 'Are you sure you want to delete this document?', + } +); +const DELETE_SUCCESS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccess', + { + defaultMessage: 'Successfully marked document for deletion. It will be deleted momentarily.', + } +); + +export const DocumentDetailLogic = kea({ + path: ['enterprise_search', 'app_search', 'document_detail_logic'], + actions: () => ({ + setFields: (fields) => ({ fields }), + getDocumentDetails: (documentId) => ({ documentId }), + deleteDocument: (documentId) => ({ documentId }), + }), + reducers: () => ({ + dataLoading: [ + true, + { + setFields: () => false, + }, + ], + fields: [ + [], + { + setFields: (_, { fields }) => fields, + }, + ], + }), + listeners: ({ actions }) => ({ + getDocumentDetails: async ({ documentId }) => { + const { engineName } = EngineLogic.values; + + try { + const { http } = HttpLogic.values; + // TODO: Handle 404s + const response = await http.get( + `/api/app_search/engines/${engineName}/documents/${documentId}` + ); + actions.setFields(response.fields); + } catch (e) { + flashAPIErrors(e); + } + }, + deleteDocument: async ({ documentId }) => { + const { engineName } = EngineLogic.values; + + if (window.confirm(CONFIRM_DELETE)) { + try { + const { http } = HttpLogic.values; + await http.delete(`/api/app_search/engines/${engineName}/documents/${documentId}`); + setQueuedSuccessMessage(DELETE_SUCCESS); + // TODO Handle routing after success + } catch (e) { + flashAPIErrors(e); + } + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts new file mode 100644 index 0000000000000..236172f0f7bdf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { DocumentsLogic } from './documents_logic'; + +describe('DocumentsLogic', () => { + const DEFAULT_VALUES = { + isDocumentCreationOpen: false, + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + documents_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + DocumentsLogic.mount(); + }; + + describe('actions', () => { + describe('openDocumentCreation', () => { + it('should toggle isDocumentCreationOpen to true', () => { + mount({ + isDocumentCreationOpen: false, + }); + + DocumentsLogic.actions.openDocumentCreation(); + + expect(DocumentsLogic.values).toEqual({ + ...DEFAULT_VALUES, + isDocumentCreationOpen: true, + }); + }); + }); + + describe('closeDocumentCreation', () => { + it('should toggle isDocumentCreationOpen to false', () => { + mount({ + isDocumentCreationOpen: true, + }); + + DocumentsLogic.actions.closeDocumentCreation(); + + expect(DocumentsLogic.values).toEqual({ + ...DEFAULT_VALUES, + isDocumentCreationOpen: false, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.ts new file mode 100644 index 0000000000000..dcf1a883bd3b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +interface DocumentsLogicValues { + isDocumentCreationOpen: boolean; +} + +interface DocumentsLogicActions { + closeDocumentCreation(): void; + openDocumentCreation(): void; +} + +type DocumentsLogicType = MakeLogicType; + +export const DocumentsLogic = kea({ + path: ['enterprise_search', 'app_search', 'documents_logic'], + actions: () => ({ + openDocumentCreation: true, + closeDocumentCreation: true, + }), + reducers: () => ({ + isDocumentCreationOpen: [ + false, + { + openDocumentCreation: () => true, + closeDocumentCreation: () => false, + }, + ], + }), +}); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts similarity index 61% rename from x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts index b781199c85237..d374098d70788 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: string }; -export function clearSelectedJobIdFromUrl(str: string): void; +export { DocumentDetailLogic } from './document_detail_logic'; +export { DocumentsLogic } from './documents_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts new file mode 100644 index 0000000000000..6a7c1cd1d5d2f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface FieldDetails { + name: string; + value: string | string[]; + type: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index 974e07069ddba..d77faf471facc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -19,6 +19,7 @@ describe('AppLogic', () => { account: {}, hasInitialized: false, isFederatedAuth: true, + isOrganization: false, organization: {}, }; @@ -34,6 +35,7 @@ describe('AppLogic', () => { }, hasInitialized: true, isFederatedAuth: false, + isOrganization: false, organization: { defaultOrgName: 'My Organization', name: 'ACME Donuts', @@ -61,4 +63,12 @@ describe('AppLogic', () => { }); }); }); + + describe('setContext()', () => { + it('sets context', () => { + AppLogic.actions.setContext(true); + + expect(AppLogic.values.isOrganization).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index b7476a5187749..f5f534807fabf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -16,9 +16,11 @@ import { interface AppValues extends WorkplaceSearchInitialData { hasInitialized: boolean; isFederatedAuth: boolean; + isOrganization: boolean; } interface AppActions { initializeAppData(props: InitialAppData): InitialAppData; + setContext(isOrganization: boolean): boolean; } const emptyOrg = {} as Organization; @@ -31,6 +33,7 @@ export const AppLogic = kea>({ workplaceSearch, isFederatedAuth, }), + setContext: (isOrganization) => isOrganization, }, reducers: { hasInitialized: [ @@ -45,6 +48,12 @@ export const AppLogic = kea>({ initializeAppData: (_, { isFederatedAuth }) => !!isFederatedAuth, }, ], + isOrganization: [ + false, + { + setContext: (_, isOrganization) => isOrganization, + }, + ], organization: [ emptyOrg, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 25544b4a9bb68..5f1e2dd18d3b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -46,9 +46,12 @@ describe('WorkplaceSearchUnconfigured', () => { }); describe('WorkplaceSearchConfigured', () => { + const initializeAppData = jest.fn(); + const setContext = jest.fn(); + beforeEach(() => { jest.clearAllMocks(); - setMockActions({ initializeAppData: () => {} }); + setMockActions({ initializeAppData, setContext }); }); it('renders layout and header actions', () => { @@ -60,17 +63,12 @@ describe('WorkplaceSearchConfigured', () => { }); it('initializes app data with passed props', () => { - const initializeAppData = jest.fn(); - setMockActions({ initializeAppData }); - shallow(); expect(initializeAppData).toHaveBeenCalledWith({ isFederatedAuth: true }); }); it('does not re-initialize app data or re-render header actions', () => { - const initializeAppData = jest.fn(); - setMockActions({ initializeAppData }); setMockValues({ hasInitialized: true }); shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 311f30a891eb9..776cae24dfdfb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect } from 'react'; -import { Route, Redirect, Switch } from 'react-router-dom'; +import { Route, Redirect, Switch, useLocation } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; @@ -31,10 +31,21 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); - const { initializeAppData } = useActions(AppLogic); + const { initializeAppData, setContext } = useActions(AppLogic); const { renderHeaderActions } = useValues(KibanaLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); + const { pathname } = useLocation(); + + /** + * Personal dashboard urls begin with /p/ + * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources + */ + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + + // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. + const isOrganization = !pathname.match(personalSourceUrlRegex); + useEffect(() => { if (!hasInitialized) { initializeAppData(props); @@ -42,6 +53,10 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { } }, [hasInitialized]); + useEffect(() => { + setContext(isOrganization); + }, [isOrganization]); + return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index f09160d513344..801bcda2a319a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -36,6 +36,41 @@ export interface User { groupIds: string[]; } +export interface Features { + basicOrgContext?: FeatureIds[]; + basicOrgContextExcludedFeatures?: FeatureIds[]; + platinumOrgContext?: FeatureIds[]; + platinumPrivateContext: FeatureIds[]; +} + +export interface Configuration { + isPublicKey: boolean; + needsBaseUrl: boolean; + needsSubdomain?: boolean; + needsConfiguration?: boolean; + hasOauthRedirect: boolean; + baseUrlTitle?: string; + helpText: string; + documentationUrl: string; + applicationPortalUrl?: string; + applicationLinkTitle?: string; +} + +export interface SourceDataItem { + name: string; + serviceType: string; + configuration: Configuration; + configured?: boolean; + connected?: boolean; + features?: Features; + objTypes?: string[]; + sourceDescription: string; + connectStepDescription: string; + addPath: string; + editPath: string; + accountContextOnly: boolean; +} + export interface ContentSource { id: string; serviceType: string; @@ -54,6 +89,25 @@ export interface ContentSourceDetails extends ContentSource { boost: number; } +export interface ContentSourceStatus { + id: string; + name: string; + service_type: string; + status: { + status: string; + synced_at: string; + error_reason: number; + }; +} + +export interface Connector { + serviceType: string; + name: string; + configured: boolean; + supportedByLicense: boolean; + accountContextOnly: boolean; +} + export interface SourcePriority { [id: string]: number; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index d04b2cb16d308..dff9895dd84f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -59,7 +59,7 @@ import { CUSTOM_SOURCE_DOCS_URL, } from '../../routes'; -import { FeatureIds } from '../../types'; +import { FeatureIds, SourceDataItem } from '../../types'; import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; @@ -740,4 +740,4 @@ export const staticSourceData = [ connectStepDescription: connectStepDescription.empty, accountContextOnly: false, }, -]; +] as SourceDataItem[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts new file mode 100644 index 0000000000000..eacba312d5da6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, findIndex } from 'lodash'; + +import { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../../../shared/http'; + +import { + flashAPIErrors, + setSuccessMessage, + FlashMessagesLogic, +} from '../../../shared/flash_messages'; + +import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; + +import { staticSourceData } from './source_data'; + +import { AppLogic } from '../../app_logic'; + +const ORG_SOURCES_PATH = '/api/workplace_search/org/sources'; +const ACCOUNT_SOURCES_PATH = '/api/workplace_search/account/sources'; + +interface ServerStatuses { + [key: string]: string; +} + +export interface ISourcesActions { + setServerSourceStatuses(statuses: ContentSourceStatus[]): ContentSourceStatus[]; + onInitializeSources(serverResponse: ISourcesServerResponse): ISourcesServerResponse; + onSetSearchability( + sourceId: string, + searchable: boolean + ): { sourceId: string; searchable: boolean }; + setAddedSource( + addedSourceName: string, + additionalConfiguration: boolean, + serviceType: string + ): { addedSourceName: string; additionalConfiguration: boolean; serviceType: string }; + resetFlashMessages(): void; + resetPermissionsModal(): void; + resetSourcesState(): void; + initializeSources(): void; + pollForSourceStatusChanges(): void; + setSourceSearchability( + sourceId: string, + searchable: boolean + ): { sourceId: string; searchable: boolean }; +} + +export interface IPermissionsModalProps { + addedSourceName: string; + serviceType: string; + additionalConfiguration: boolean; +} + +type CombinedDataItem = SourceDataItem & ContentSourceDetails; + +export interface ISourcesValues { + contentSources: ContentSourceDetails[]; + privateContentSources: ContentSourceDetails[]; + sourceData: CombinedDataItem[]; + availableSources: SourceDataItem[]; + configuredSources: SourceDataItem[]; + serviceTypes: Connector[]; + permissionsModal: IPermissionsModalProps | null; + dataLoading: boolean; + serverStatuses: ServerStatuses | null; +} + +interface ISourcesServerResponse { + contentSources: ContentSourceDetails[]; + privateContentSources?: ContentSourceDetails[]; + serviceTypes: Connector[]; +} + +export const SourcesLogic = kea>({ + actions: { + setServerSourceStatuses: (statuses: ContentSourceStatus[]) => statuses, + onInitializeSources: (serverResponse: ISourcesServerResponse) => serverResponse, + onSetSearchability: (sourceId: string, searchable: boolean) => ({ sourceId, searchable }), + setAddedSource: ( + addedSourceName: string, + additionalConfiguration: boolean, + serviceType: string + ) => ({ addedSourceName, additionalConfiguration, serviceType }), + resetFlashMessages: () => true, + resetPermissionsModal: () => true, + resetSourcesState: () => true, + initializeSources: () => true, + pollForSourceStatusChanges: () => true, + setSourceSearchability: (sourceId: string, searchable: boolean) => ({ sourceId, searchable }), + }, + reducers: { + contentSources: [ + [], + { + onInitializeSources: (_, { contentSources }) => contentSources, + onSetSearchability: (contentSources, { sourceId, searchable }) => + updateSourcesOnToggle(contentSources, sourceId, searchable), + }, + ], + privateContentSources: [ + [], + { + onInitializeSources: (_, { privateContentSources }) => privateContentSources || [], + onSetSearchability: (privateContentSources, { sourceId, searchable }) => + updateSourcesOnToggle(privateContentSources, sourceId, searchable), + }, + ], + serviceTypes: [ + [], + { + onInitializeSources: (_, { serviceTypes }) => serviceTypes || [], + }, + ], + permissionsModal: [ + null, + { + setAddedSource: (_, data) => data, + resetPermissionsModal: () => null, + }, + ], + dataLoading: [ + true, + { + onInitializeSources: () => false, + resetSourcesState: () => true, + }, + ], + serverStatuses: [ + null, + { + setServerSourceStatuses: (_, sources) => { + const serverStatuses = {} as ServerStatuses; + sources.forEach((source) => { + serverStatuses[source.id as string] = source.status.status; + }); + return serverStatuses; + }, + }, + ], + }, + selectors: ({ selectors }) => ({ + availableSources: [ + () => [selectors.sourceData], + (sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => !configured), + ], + configuredSources: [ + () => [selectors.sourceData], + (sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => configured), + ], + sourceData: [ + () => [selectors.serviceTypes, selectors.contentSources], + (serviceTypes, contentSources) => + mergeServerAndStaticData(serviceTypes, staticSourceData, contentSources), + ], + }), + listeners: ({ actions, values }) => ({ + initializeSources: async () => { + const { isOrganization } = AppLogic.values; + const route = isOrganization ? ORG_SOURCES_PATH : ACCOUNT_SOURCES_PATH; + + try { + const response = await HttpLogic.values.http.get(route); + actions.onInitializeSources(response); + } catch (e) { + flashAPIErrors(e); + } + + if (isOrganization && !values.serverStatuses) { + // We want to get the initial statuses from the server to compare our polling results to. + const sourceStatuses = await fetchSourceStatuses(isOrganization); + actions.setServerSourceStatuses(sourceStatuses); + } + }, + // We poll the server and if the status update, we trigger a new fetch of the sources. + pollForSourceStatusChanges: async () => { + const { isOrganization } = AppLogic.values; + if (!isOrganization) return; + const serverStatuses = values.serverStatuses; + + const sourceStatuses = await fetchSourceStatuses(isOrganization); + + sourceStatuses.some((source: ContentSourceStatus) => { + if (serverStatuses && serverStatuses[source.id] !== source.status.status) { + return actions.initializeSources(); + } + }); + }, + setSourceSearchability: async ({ sourceId, searchable }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/searchable` + : `/api/workplace_search/account/sources/${sourceId}/searchable`; + + try { + await HttpLogic.values.http.put(route, { + body: JSON.stringify({ searchable }), + }); + actions.onSetSearchability(sourceId, searchable); + } catch (e) { + flashAPIErrors(e); + } + }, + setAddedSource: ({ addedSourceName, additionalConfiguration }) => { + setSuccessMessage( + [ + `Successfully connected ${addedSourceName}.`, + additionalConfiguration ? 'This source requires additional configuration.' : '', + ].join(' ') + ); + }, + resetFlashMessages: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + }), +}); + +const fetchSourceStatuses = async (isOrganization: boolean) => { + const route = isOrganization ? ORG_SOURCES_PATH : ACCOUNT_SOURCES_PATH; + let response; + + try { + response = await HttpLogic.values.http.get(route); + SourcesLogic.actions.setServerSourceStatuses(response); + } catch (e) { + flashAPIErrors(e); + } + + return response; +}; + +const updateSourcesOnToggle = ( + contentSources: ContentSourceDetails[], + sourceId: string, + searchable: boolean +): ContentSourceDetails[] => { + if (!contentSources) return []; + const sources = cloneDeep(contentSources) as ContentSourceDetails[]; + const index = findIndex(sources, ({ id }) => id === sourceId); + const updatedSource = sources[index]; + sources[index] = { + ...updatedSource, + searchable, + }; + return sources; +}; + +/** + * We have 3 different data sets we have to combine in the UI. The first is the static (`staticSourceData`) + * data that contains the UI componets, such as the Path for React Router and the copy and images. + * + * The second is the base list of available sources that the server sends back in the collection, + * `availableTypes` that is the source of truth for the name and whether the source has been configured. + * + * Fnally, also in the collection response is the current set of connected sources. We check for the + * existence of a `connectedSource` of the type in the loop and set `connected` to true so that the UI + * can diplay "Add New" instead of "Connect", the latter of which is displated only when a connector + * has been configured but there are no connected sources yet. + */ +const mergeServerAndStaticData = ( + serverData: ContentSourceDetails[], + staticData: SourceDataItem[], + contentSources: ContentSourceDetails[] +) => { + const combined = [] as CombinedDataItem[]; + serverData.forEach((serverItem) => { + const type = serverItem.serviceType; + const staticItem = staticData.find(({ serviceType }) => serviceType === type); + const connectedSource = contentSources.find(({ serviceType }) => serviceType === type); + combined.push({ + ...serverItem, + ...staticItem, + connected: !!connectedSource, + } as CombinedDataItem); + }); + + return combined; +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts new file mode 100644 index 0000000000000..d5fed4c6f97cb --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerDocumentRoutes } from './documents'; + +describe('document routes', () => { + describe('GET /api/app_search/engines/{engineName}/documents/{documentId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + }); + + registerDocumentRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/documents/1', + }); + }); + }); + + describe('DELETE /api/app_search/engines/{engineName}/documents/{documentId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + }); + + registerDocumentRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/documents/1', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts new file mode 100644 index 0000000000000..a2f4b323a91aa --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerDocumentRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + validate: { + params: schema.object({ + engineName: schema.string(), + documentId: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`, + })(context, request, response); + } + ); + router.delete( + { + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + validate: { + params: schema.object({ + engineName: schema.string(), + documentId: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`, + })(context, request, response); + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index faf74203cf17d..f64e45c656fa1 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -9,9 +9,11 @@ import { RouteDependencies } from '../../plugin'; import { registerEnginesRoutes } from './engines'; import { registerCredentialsRoutes } from './credentials'; import { registerSettingsRoutes } from './settings'; +import { registerDocumentRoutes } from './documents'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerEnginesRoutes(dependencies); registerCredentialsRoutes(dependencies); registerSettingsRoutes(dependencies); + registerDocumentRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 6d22002222a66..9cf491b79fd24 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -411,7 +411,7 @@ describe('sources routes', () => { }); }); - describe('PUT /api/workplace_search/sources/{id}/searchable', () => { + describe('PUT /api/workplace_search/account/sources/{id}/searchable', () => { let mockRouter: MockRouter; beforeEach(() => { @@ -421,7 +421,7 @@ describe('sources routes', () => { it('creates a request handler', () => { mockRouter = new MockRouter({ method: 'put', - path: '/api/workplace_search/sources/{id}/searchable', + path: '/api/workplace_search/account/sources/{id}/searchable', payload: 'body', }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index efef53440117e..bdd048438dae5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -268,7 +268,7 @@ export function registerAccountSourceSearchableRoute({ }: RouteDependencies) { router.put( { - path: '/api/workplace_search/sources/{id}/searchable', + path: '/api/workplace_search/account/sources/{id}/searchable', validate: { body: schema.object({ searchable: schema.boolean(), diff --git a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts index 102324c18bd43..280c34744289e 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts @@ -3,21 +3,57 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { pkgToPkgKey } from '../registry/index'; +import { ArchiveEntry } from './index'; +import { InstallSource, ArchivePackage, RegistryPackage } from '../../../../common'; -const cache: Map = new Map(); -export const cacheGet = (key: string) => cache.get(key); -export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); -export const cacheHas = (key: string) => cache.has(key); -export const cacheClear = () => cache.clear(); -export const cacheDelete = (key: string) => cache.delete(key); +const archiveEntryCache: Map = new Map(); +export const getArchiveEntry = (key: string) => archiveEntryCache.get(key); +export const setArchiveEntry = (key: string, value: Buffer) => archiveEntryCache.set(key, value); +export const hasArchiveEntry = (key: string) => archiveEntryCache.has(key); +export const clearArchiveEntries = () => archiveEntryCache.clear(); +export const deleteArchiveEntry = (key: string) => archiveEntryCache.delete(key); -const archiveFilelistCache: Map = new Map(); -export const getArchiveFilelist = (name: string, version: string) => - archiveFilelistCache.get(pkgToPkgKey({ name, version })); +export interface SharedKey { + name: string; + version: string; + installSource: InstallSource; +} +type SharedKeyString = string; -export const setArchiveFilelist = (name: string, version: string, paths: string[]) => - archiveFilelistCache.set(pkgToPkgKey({ name, version }), paths); +type ArchiveFilelist = string[]; +const archiveFilelistCache: Map = new Map(); +export const getArchiveFilelist = (keyArgs: SharedKey) => + archiveFilelistCache.get(sharedKey(keyArgs)); -export const deleteArchiveFilelist = (name: string, version: string) => - archiveFilelistCache.delete(pkgToPkgKey({ name, version })); +export const setArchiveFilelist = (keyArgs: SharedKey, paths: string[]) => + archiveFilelistCache.set(sharedKey(keyArgs), paths); + +export const deleteArchiveFilelist = (keyArgs: SharedKey) => + archiveFilelistCache.delete(sharedKey(keyArgs)); + +const packageInfoCache: Map = new Map(); +const sharedKey = ({ name, version, installSource }: SharedKey) => + `${name}-${version}-${installSource}`; + +export const getPackageInfo = (args: SharedKey) => { + const packageInfo = packageInfoCache.get(sharedKey(args)); + if (args.installSource === 'registry') { + return packageInfo as RegistryPackage; + } else if (args.installSource === 'upload') { + return packageInfo as ArchivePackage; + } else { + throw new Error(`Unknown installSource: ${args.installSource}`); + } +}; + +export const setPackageInfo = ({ + name, + version, + installSource, + packageInfo, +}: SharedKey & { packageInfo: ArchivePackage | RegistryPackage }) => { + const key = sharedKey({ name, version, installSource }); + return packageInfoCache.set(key, packageInfo); +}; + +export const deletePackageInfo = (args: SharedKey) => packageInfoCache.delete(sharedKey(args)); diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index 6d1150b3ac8bd..ddaf9b640c86a 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -4,68 +4,57 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ArchivePackage, AssetParts } from '../../../../common/types'; +import { AssetParts, InstallSource } from '../../../../common/types'; import { PackageInvalidArchiveError, PackageUnsupportedMediaTypeError } from '../../../errors'; import { - cacheGet, - cacheSet, - cacheDelete, + SharedKey, + getArchiveEntry, + setArchiveEntry, + deleteArchiveEntry, getArchiveFilelist, setArchiveFilelist, deleteArchiveFilelist, + deletePackageInfo, } from './cache'; import { getBufferExtractor } from './extract'; -import { parseAndVerifyArchiveEntries } from './validation'; export * from './cache'; -export { untarBuffer, unzipBuffer, getBufferExtractor } from './extract'; +export { getBufferExtractor, untarBuffer, unzipBuffer } from './extract'; +export { parseAndVerifyArchiveBuffer as parseAndVerifyArchiveEntries } from './validation'; export interface ArchiveEntry { path: string; buffer?: Buffer; } -export async function getArchivePackage({ - archiveBuffer, +export async function unpackBufferToCache({ + name, + version, contentType, + archiveBuffer, + installSource, }: { - archiveBuffer: Buffer; + name: string; + version: string; contentType: string; -}): Promise<{ paths: string[]; archivePackageInfo: ArchivePackage }> { - const entries = await unpackArchiveEntries(archiveBuffer, contentType); - const { archivePackageInfo } = await parseAndVerifyArchiveEntries(entries); - const paths = addEntriesToMemoryStore(entries); - - setArchiveFilelist(archivePackageInfo.name, archivePackageInfo.version, paths); - - return { - paths, - archivePackageInfo, - }; -} - -export async function unpackArchiveToCache( - archiveBuffer: Buffer, - contentType: string -): Promise { - const entries = await unpackArchiveEntries(archiveBuffer, contentType); - return addEntriesToMemoryStore(entries); -} - -function addEntriesToMemoryStore(entries: ArchiveEntry[]) { + archiveBuffer: Buffer; + installSource: InstallSource; +}): Promise { + const entries = await unpackBufferEntries(archiveBuffer, contentType); const paths: string[] = []; entries.forEach((entry) => { const { path, buffer } = entry; if (buffer) { - cacheSet(path, buffer); + setArchiveEntry(path, buffer); paths.push(path); } }); + setArchiveFilelist({ name, version, installSource }, paths); return paths; } -export async function unpackArchiveEntries( +export async function unpackBufferEntries( archiveBuffer: Buffer, contentType: string ): Promise { @@ -96,16 +85,18 @@ export async function unpackArchiveEntries( return entries; } -export const deletePackageCache = (name: string, version: string) => { +export const deletePackageCache = ({ name, version, installSource }: SharedKey) => { // get cached archive filelist - const paths = getArchiveFilelist(name, version); + const paths = getArchiveFilelist({ name, version, installSource }); // delete cached archive filelist - deleteArchiveFilelist(name, version); + deleteArchiveFilelist({ name, version, installSource }); // delete cached archive files - // this has been populated in unpackArchiveToCache() - paths?.forEach((path) => cacheDelete(path)); + // this has been populated in unpackBufferToCache() + paths?.forEach(deleteArchiveEntry); + + deletePackageInfo({ name, version, installSource }); }; export function getPathParts(path: string): AssetParts { @@ -139,7 +130,7 @@ export function getPathParts(path: string): AssetParts { } export function getAsset(key: string) { - const buffer = cacheGet(key); + const buffer = getArchiveEntry(key); if (buffer === undefined) throw new Error(`Cannot find asset ${key}`); return buffer; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index 992020cb073ad..dc7a91e08799c 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -15,7 +15,7 @@ import { RegistryVarsEntry, } from '../../../../common/types'; import { PackageInvalidArchiveError } from '../../../errors'; -import { ArchiveEntry } from './index'; +import { unpackBufferEntries } from './index'; import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record = {}; @@ -24,9 +24,11 @@ const MANIFEST_NAME = 'manifest.yml'; // TODO: everything below performs verification of manifest.yml files, and hence duplicates functionality already implemented in the // package registry. At some point this should probably be replaced (or enhanced) with verification based on // https://github.com/elastic/package-spec/ -export async function parseAndVerifyArchiveEntries( - entries: ArchiveEntry[] -): Promise<{ paths: string[]; archivePackageInfo: ArchivePackage }> { +export async function parseAndVerifyArchiveBuffer( + archiveBuffer: Buffer, + contentType: string +): Promise<{ paths: string[]; packageInfo: ArchivePackage }> { + const entries = await unpackBufferEntries(archiveBuffer, contentType); const paths: string[] = []; entries.forEach(({ path, buffer }) => { paths.push(path); @@ -34,12 +36,12 @@ export async function parseAndVerifyArchiveEntries( }); return { - archivePackageInfo: parseAndVerifyArchive(paths), + packageInfo: parseAndVerifyArchive(paths), paths, }; } -export function parseAndVerifyArchive(paths: string[]): ArchivePackage { +function parseAndVerifyArchive(paths: string[]): ArchivePackage { // The top-level directory must match pkgName-pkgVersion, and no other top-level files or directories may be present const toplevelDir = paths[0].split('/')[0]; paths.forEach((path) => { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts index d18f43d62436a..ee5257b8a3ef6 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts @@ -85,14 +85,6 @@ export async function installIndexPatterns( savedObjectsClient, installationStatuses.Installed ); - // TODO: move to install package - // cache all installed packages if they don't exist - const packagePromises = installedPackages.map((pkg) => - // TODO: this hard-codes 'registry' as installSource, so uploaded packages are ignored - // and their fields will be removed from the generated index patterns after this runs. - Registry.ensureCachedArchiveInfo(pkg.pkgName, pkg.pkgVersion, 'registry') - ); - await Promise.all(packagePromises); const packageVersionsToFetch = [...installedPackages]; if (pkgName && pkgVersion) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 80e1cbba6484b..770f342c0a6e7 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -5,7 +5,6 @@ */ import { InstallablePackage } from '../../../types'; -import * as Registry from '../registry'; import { ArchiveEntry, getArchiveFilelist, getAsset } from '../archive'; // paths from RegistryPackage are routes to the assets on EPR @@ -21,7 +20,8 @@ export function getAssets( datasetName?: string ): string[] { const assets: string[] = []; - const paths = getArchiveFilelist(packageInfo.name, packageInfo.version); + const { name, version } = packageInfo; + const paths = getArchiveFilelist({ name, version, installSource: 'registry' }); // TODO: might be better to throw a PackageCacheError here if (!paths || paths.length === 0) return assets; @@ -47,15 +47,13 @@ export function getAssets( return assets; } +// ASK: Does getAssetsData need an installSource now? +// if so, should it be an Installation vs InstallablePackage or add another argument? export async function getAssetsData( packageInfo: InstallablePackage, filter = (path: string): boolean => true, datasetName?: string ): Promise { - // TODO: Needs to be called to fill the cache but should not be required - - await Registry.ensureCachedArchiveInfo(packageInfo.name, packageInfo.version, 'registry'); - // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); const entries: ArchiveEntry[] = assets.map((path) => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 893df1733c58b..2d4a94a2332d6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -109,11 +109,7 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [ - savedObject, - latestPackage, - { paths: assets, registryPackageInfo: item }, - ] = await Promise.all([ + const [savedObject, latestPackage, { paths: assets, packageInfo: item }] = await Promise.all([ getInstallationObject({ savedObjectsClient, pkgName }), Registry.fetchFindLatestPackage(pkgName), Registry.getRegistryPackage(pkgName, pkgVersion), diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index c471ea732b9dc..e73a5d3533828 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -28,6 +28,7 @@ import { KibanaAssetType, } from '../../../types'; import * as Registry from '../registry'; +import { setPackageInfo, parseAndVerifyArchiveEntries, unpackBufferToCache } from '../archive'; import { getInstallation, getInstallationObject, @@ -43,7 +44,6 @@ import { } from '../../../errors'; import { getPackageSavedObjects } from './get'; import { appContextService } from '../../app_context'; -import { getArchivePackage } from '../archive'; import { _installPackage } from './_install_package'; export async function installLatestPackage(options: { @@ -245,29 +245,26 @@ async function installPackageFromRegistry({ }: InstallRegistryPackageParams): Promise { // TODO: change epm API to /packageName/version so we don't need to do this const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); - // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge - // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets - const latestPackage = await Registry.fetchFindLatestPackage(pkgName); // get the currently installed package const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installType = getInstallType({ pkgVersion, installedPkg }); - // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update const installOutOfDateVersionOk = installType === 'reinstall' || installType === 'reupdate' || installType === 'rollback'; + + const latestPackage = await Registry.fetchFindLatestPackage(pkgName); if (semverLt(pkgVersion, latestPackage.version) && !force && !installOutOfDateVersionOk) { throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); } - const { paths, registryPackageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); + const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); return _installPackage({ savedObjectsClient, callCluster, installedPkg, paths, - packageInfo: registryPackageInfo, + packageInfo, installType, installSource: 'registry', }); @@ -290,27 +287,44 @@ async function installPackageByUpload({ archiveBuffer, contentType, }: InstallUploadedArchiveParams): Promise { - const { paths, archivePackageInfo } = await getArchivePackage({ archiveBuffer, contentType }); + const { packageInfo } = await parseAndVerifyArchiveEntries(archiveBuffer, contentType); const installedPkg = await getInstallationObject({ savedObjectsClient, - pkgName: archivePackageInfo.name, + pkgName: packageInfo.name, }); - const installType = getInstallType({ pkgVersion: archivePackageInfo.version, installedPkg }); + + const installType = getInstallType({ pkgVersion: packageInfo.version, installedPkg }); if (installType !== 'install') { throw new PackageOperationNotSupportedError( - `Package upload only supports fresh installations. Package ${archivePackageInfo.name} is already installed, please uninstall first.` + `Package upload only supports fresh installations. Package ${packageInfo.name} is already installed, please uninstall first.` ); } + const installSource = 'upload'; + const paths = await unpackBufferToCache({ + name: packageInfo.name, + version: packageInfo.version, + installSource, + archiveBuffer, + contentType, + }); + + setPackageInfo({ + name: packageInfo.name, + version: packageInfo.version, + installSource, + packageInfo, + }); + return _installPackage({ savedObjectsClient, callCluster, installedPkg, paths, - packageInfo: archivePackageInfo, + packageInfo, installType, - installSource: 'upload', + installSource, }); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 9fabbaf72474e..ca84980107fe3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -63,7 +63,11 @@ export async function removeInstallation(options: { // remove the package archive and its contents from the cache so that a reinstall fetches // a fresh copy from the registry - deletePackageCache(pkgName, pkgVersion); + deletePackageCache({ + name: pkgName, + version: pkgVersion, + installSource: installation.install_source, + }); // successful delete's in SO client return {}. return something more useful return installedAssets; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index a6f42ebb96752..2d496055df78a 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -20,8 +20,9 @@ import { import { getArchiveFilelist, getPathParts, - setArchiveFilelist, - unpackArchiveToCache, + unpackBufferToCache, + getPackageInfo, + setPackageInfo, } from '../archive'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from '../streams'; @@ -126,25 +127,44 @@ export async function fetchCategories(params?: CategoriesParams): Promise { - let paths = getArchiveFilelist(pkgName, pkgVersion); + name: string, + version: string +): Promise<{ paths: string[]; packageInfo: RegistryPackage }> { + const installSource = 'registry'; + let paths = getArchiveFilelist({ name, version, installSource }); if (!paths || paths.length === 0) { - const { archiveBuffer, archivePath } = await fetchArchiveBuffer(pkgName, pkgVersion); - const contentType = mime.lookup(archivePath); - if (!contentType) { - throw new Error(`Unknown compression format for '${archivePath}'. Please use .zip or .gz`); - } - paths = await unpackArchiveToCache(archiveBuffer, contentType); - setArchiveFilelist(pkgName, pkgVersion, paths); + const { archiveBuffer, archivePath } = await fetchArchiveBuffer(name, version); + paths = await unpackBufferToCache({ + name, + version, + installSource, + archiveBuffer, + contentType: ensureContentType(archivePath), + }); } - // TODO: cache this as well? - const registryPackageInfo = await fetchInfo(pkgName, pkgVersion); + const packageInfo = await getInfo(name, version); - return { paths, registryPackageInfo }; + return { paths, packageInfo }; +} + +function ensureContentType(archivePath: string) { + const contentType = mime.lookup(archivePath); + if (!contentType) { + throw new Error(`Unknown compression format for '${archivePath}'. Please use .zip or .gz`); + } + return contentType; } export async function ensureCachedArchiveInfo( @@ -152,7 +172,7 @@ export async function ensureCachedArchiveInfo( version: string, installSource: InstallSource = 'registry' ) { - const paths = getArchiveFilelist(name, version); + const paths = getArchiveFilelist({ name, version, installSource }); if (!paths || paths.length === 0) { if (installSource === 'registry') { await getRegistryPackage(name, version); @@ -168,7 +188,7 @@ async function fetchArchiveBuffer( pkgName: string, pkgVersion: string ): Promise<{ archiveBuffer: Buffer; archivePath: string }> { - const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); + const { download: archivePath } = await getInfo(pkgName, pkgVersion); const archiveUrl = `${getRegistryUrl()}${archivePath}`; const archiveBuffer = await getResponseStream(archiveUrl).then(streamToBuffer); diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 5cf43d2830489..7e6e6d5e408b4 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -44,6 +44,7 @@ export { EpmPackageInstallStatus, InstallationStatus, PackageInfo, + ArchivePackage, RegistryVarsEntry, RegistryDataStream, RegistryElasticsearch, diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index d82c7b092c38a..0af8e01d7290d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -284,7 +284,7 @@ describe('Datatable Visualization', () => { state: { layers: [layer] }, frame, }).groups[1].accessors - ).toEqual(['c', 'b']); + ).toEqual([{ columnId: 'c' }, { columnId: 'b' }]); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index e0f6ae31719ca..8b5d2d7d73348 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -149,9 +149,9 @@ export const datatableVisualization: Visualization defaultMessage: 'Break down by', }), layerId: state.layers[0].layerId, - accessors: sortedColumns.filter( - (c) => datasource!.getOperationForColumnId(c)?.isBucketed - ), + accessors: sortedColumns + .filter((c) => datasource!.getOperationForColumnId(c)?.isBucketed) + .map((accessor) => ({ columnId: accessor })), supportsMoreColumns: true, filterOperations: (op) => op.isBucketed, dataTestSubj: 'lnsDatatable_column', @@ -162,9 +162,9 @@ export const datatableVisualization: Visualization defaultMessage: 'Metrics', }), layerId: state.layers[0].layerId, - accessors: sortedColumns.filter( - (c) => !datasource!.getOperationForColumnId(c)?.isBucketed - ), + accessors: sortedColumns + .filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed) + .map((accessor) => ({ columnId: accessor })), supportsMoreColumns: true, filterOperations: (op) => !op.isBucketed, required: true, diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index 8766c9f0acabf..ded0b4552a4e5 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -47,7 +47,7 @@ // Drop area will be replacing existing content .lnsDragDrop-isReplacing { &, - .lnsLayerPanel__triggerLink { + .lnsLayerPanel__triggerText { text-decoration: line-through; } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx new file mode 100644 index 0000000000000..5ee1139ff09a2 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AccessorConfig } from '../../../types'; + +export function ColorIndicator({ + accessorConfig, + children, +}: { + accessorConfig: AccessorConfig; + children: React.ReactChild; +}) { + let indicatorIcon = null; + if (accessorConfig.triggerIcon && accessorConfig.triggerIcon !== 'none') { + const baseIconProps = { + size: 's', + className: 'lnsLayerPanel__colorIndicator', + } as const; + + indicatorIcon = ( + + {accessorConfig.triggerIcon === 'color' && accessorConfig.color && ( + + )} + {accessorConfig.triggerIcon === 'disabled' && ( + + )} + {accessorConfig.triggerIcon === 'colorBy' && ( + + )} + + ); + } + + return ( + + {indicatorIcon} + {children} + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index b98d5b748edb8..a1a072be77f81 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -52,6 +52,7 @@ align-items: center; overflow: hidden; min-height: $euiSizeXXL; + position: relative; // NativeRenderer is messing this up > div { @@ -80,28 +81,18 @@ margin-right: $euiSizeS; } -.lnsLayerPanel__triggerLink { +.lnsLayerPanel__triggerText { width: 100%; padding: $euiSizeS; min-height: $euiSizeXXL - 2; word-break: break-word; - - &:focus { - background-color: transparent !important; // sass-lint:disable-line no-important - outline: none !important; // sass-lint:disable-line no-important - } - - &:focus .lnsLayerPanel__triggerLinkLabel, - &:focus-within .lnsLayerPanel__triggerLinkLabel { - background-color: transparentize($euiColorVis1, .9); - } } -.lnsLayerPanel__triggerLinkLabel { +.lnsLayerPanel__triggerTextLabel { transition: background-color $euiAnimSpeedFast ease-in-out; } -.lnsLayerPanel__triggerLinkContent { +.lnsLayerPanel__triggerTextContent { // Make EUI button content not centered justify-content: flex-start; padding: 0 !important; // sass-lint:disable-line no-important @@ -111,3 +102,32 @@ .lnsLayerPanel__styleEditor { padding: 0 $euiSizeS $euiSizeS; } + +.lnsLayerPanel__colorIndicator { + margin-left: $euiSizeS; +} + +.lnsLayerPanel__paletteContainer { + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.lnsLayerPanel__paletteColor { + height: $euiSizeXS; +} + +.lnsLayerPanel__dimensionLink { + width: 100%; + + &:focus { + background-color: transparent !important; // sass-lint:disable-line no-important + outline: none !important; // sass-lint:disable-line no-important + } + + &:focus .lnsLayerPanel__triggerTextLabel, + &:focus-within .lnsLayerPanel__triggerTextLabel { + background-color: transparentize($euiColorVis1, .9); + } +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index f440042801ca6..37dc039df498b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -137,7 +137,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['x'], + accessors: [{ columnId: 'x' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -177,7 +177,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['x'], + accessors: [{ columnId: 'x' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -209,7 +209,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['newid'], + accessors: [{ columnId: 'newid' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroup', @@ -257,7 +257,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['newid'], + accessors: [{ columnId: 'newid' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -302,7 +302,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['newid'], + accessors: [{ columnId: 'newid' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -377,7 +377,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['a'], + accessors: [{ columnId: 'a' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroup', @@ -416,7 +416,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['a'], + accessors: [{ columnId: 'a' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroupA', @@ -424,7 +424,7 @@ describe('LayerPanel', () => { { groupLabel: 'B', groupId: 'b', - accessors: ['b'], + accessors: [{ columnId: 'b' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroupB', @@ -480,7 +480,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['a', 'b'], + accessors: [{ columnId: 'a' }, { columnId: 'b' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroup', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index f780f9c3f22d7..f5b31fb881167 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiButtonEmpty, EuiFormRow, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -25,6 +26,8 @@ import { trackUiEvent } from '../../../lens_ui_telemetry'; import { generateId } from '../../../id_generator'; import { ConfigPanelWrapperProps, ActiveDimensionState } from './types'; import { DimensionContainer } from './dimension_container'; +import { ColorIndicator } from './color_indicator'; +import { PaletteIndicator } from './palette_indicator'; const initialActiveDimensionState = { isNew: false, @@ -181,6 +184,10 @@ export function LayerPanel( const newId = generateId(); const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Click to edit configuration or drag to move', + }); + return ( <> - {group.accessors.map((accessor) => { + {group.accessors.map((accessorConfig) => { + const accessor = accessorConfig.columnId; const { dragging } = dragDropContext; const dragType = isDraggedOperation(dragging) && accessor === dragging.columnId @@ -253,7 +261,9 @@ export function LayerPanel( dragType={dragType} dropType={dropType} data-test-subj={group.dataTestSubj} - itemsInGroup={group.accessors} + itemsInGroup={group.accessors.map((a) => + typeof a === 'string' ? a : a.columnId + )} className={'lnsLayerPanel__dimensionContainer'} value={{ columnId: accessor, @@ -304,25 +314,33 @@ export function LayerPanel( }} >
- { - if (activeId) { - setActiveDimension(initialActiveDimensionState); - } else { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: accessor, - }); - } - }, + { + if (activeId) { + setActiveDimension(initialActiveDimensionState); + } else { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: accessor, + }); + } }} - /> + aria-label={triggerLinkA11yText} + title={triggerLinkA11yText} + > + + + + +
); @@ -409,12 +428,12 @@ export function LayerPanel( >
{ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx new file mode 100644 index 0000000000000..7e65fe7025932 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AccessorConfig } from '../../../types'; + +export function PaletteIndicator({ accessorConfig }: { accessorConfig: AccessorConfig }) { + if (accessorConfig.triggerIcon !== 'colorBy' || !accessorConfig.palette) return null; + return ( + + {accessorConfig.palette.map((color) => ( + + ))} + + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index baf4f6bb9a6a3..94018bd84b517 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -6,7 +6,7 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiLink, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiText, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types'; @@ -66,10 +66,6 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens } const formattedLabel = wrapOnDot(uniqueLabel); - const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Click to edit configuration or drag to move', - }); - if (currentFieldIsInvalid) { return ( - @@ -101,26 +95,24 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens {selectedColumn.label} - + ); } return ( - - {formattedLabel} + {formattedLabel} - + ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index c88af50e525d4..9fbad553d441a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -188,7 +188,7 @@ describe('IndexPattern Data Source suggestions', () => { }; } - it('should apply a bucketed aggregation for a string field', () => { + it('should apply a bucketed aggregation for a string field, using metric for sorting', () => { const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { name: 'source', displayName: 'source', @@ -202,14 +202,17 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { id1: expect.objectContaining({ - columnOrder: ['id2', 'id3'], + columnOrder: ['id3', 'id2'], columns: { - id2: expect.objectContaining({ + id3: expect.objectContaining({ operationType: 'terms', sourceField: 'source', - params: expect.objectContaining({ size: 5 }), + params: expect.objectContaining({ + size: 5, + orderBy: { columnId: 'id2', type: 'column' }, + }), }), - id3: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'count', }), }, @@ -222,10 +225,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id2', + columnId: 'id3', }), expect.objectContaining({ - columnId: 'id3', + columnId: 'id2', }), ], layerId: 'id1', @@ -248,13 +251,13 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { id1: expect.objectContaining({ - columnOrder: ['id2', 'id3'], + columnOrder: ['id3', 'id2'], columns: { - id2: expect.objectContaining({ + id3: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id3: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'count', }), }, @@ -267,10 +270,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id2', + columnId: 'id3', }), expect.objectContaining({ - columnId: 'id3', + columnId: 'id2', }), ], layerId: 'id1', @@ -408,7 +411,7 @@ describe('IndexPattern Data Source suggestions', () => { }; } - it('should apply a bucketed aggregation for a string field', () => { + it('should apply a bucketed aggregation for a string field, using metric for sorting', () => { const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { name: 'source', displayName: 'source', @@ -422,14 +425,17 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { previousLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'terms', sourceField: 'source', - params: expect.objectContaining({ size: 5 }), + params: expect.objectContaining({ + size: 5, + orderBy: { columnId: 'id1', type: 'column' }, + }), }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -442,10 +448,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id1', + columnId: 'id2', }), expect.objectContaining({ - columnId: 'id2', + columnId: 'id1', }), ], layerId: 'previousLayer', @@ -468,13 +474,13 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { previousLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -487,10 +493,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id1', + columnId: 'id2', }), expect.objectContaining({ - columnId: 'id2', + columnId: 'id1', }), ], layerId: 'previousLayer', @@ -1050,13 +1056,13 @@ describe('IndexPattern Data Source suggestions', () => { layers: { previousLayer: initialState.layers.previousLayer, currentLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -1069,10 +1075,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id1', + columnId: 'id2', }), expect.objectContaining({ - columnId: 'id2', + columnId: 'id1', }), ], layerId: 'currentLayer', @@ -1097,13 +1103,13 @@ describe('IndexPattern Data Source suggestions', () => { layers: { currentLayer: initialState.layers.currentLayer, previousLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -1146,14 +1152,14 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { id1: expect.objectContaining({ - columnOrder: ['id2', 'id3'], + columnOrder: ['id3', 'id2'], columns: { - id2: expect.objectContaining({ + id3: expect.objectContaining({ operationType: 'terms', sourceField: 'source', params: expect.objectContaining({ size: 5 }), }), - id3: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'count', }), }, @@ -1166,10 +1172,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id2', + columnId: 'id3', }), expect.objectContaining({ - columnId: 'id3', + columnId: 'id2', }), ], layerId: 'id1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index b74d75207e112..ccdefee62ad5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -289,16 +289,16 @@ function createNewLayerWithBucketAggregation( operation: OperationType ): IndexPatternLayer { return insertNewColumn({ - op: 'count', + op: operation, layer: insertNewColumn({ - op: operation, + op: 'count', layer: { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] }, columnId: generateId(), - field, + field: documentField, indexPattern, }), columnId: generateId(), - field: documentField, + field, indexPattern, }); } diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index b75ac89d7e4d8..d8c475734e67e 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -96,7 +96,7 @@ export const metricVisualization: Visualization = { groupId: 'metric', groupLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), layerId: props.state.layerId, - accessors: props.state.accessor ? [props.state.accessor] : [], + accessors: props.state.accessor ? [{ columnId: props.state.accessor }] : [], supportsMoreColumns: !props.state.accessor, filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', }, diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 62e99396edbc7..91f0ddb54ad41 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -9,7 +9,7 @@ import { render } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { PaletteRegistry } from 'src/plugins/charts/public'; -import { Visualization, OperationMetadata } from '../types'; +import { Visualization, OperationMetadata, AccessorConfig } from '../types'; import { toExpression, toPreviewExpression } from './to_expression'; import { LayerState, PieVisualizationState } from './types'; import { suggestions } from './suggestions'; @@ -113,7 +113,18 @@ export const getPieVisualization = ({ .map(({ columnId }) => columnId) .filter((columnId) => columnId !== layer.metric); // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.groups))); + const sortedColumns: AccessorConfig[] = Array.from( + new Set(originalOrder.concat(layer.groups)) + ).map((accessor) => ({ columnId: accessor })); + if (sortedColumns.length > 0) { + sortedColumns[0] = { + columnId: sortedColumns[0].columnId, + triggerIcon: 'colorBy', + palette: paletteService + .get(state.palette?.name || 'default') + .getColors(10, state.palette?.params), + }; + } if (state.shape === 'treemap') { return { @@ -137,7 +148,7 @@ export const getPieVisualization = ({ defaultMessage: 'Size by', }), layerId, - accessors: layer.metric ? [layer.metric] : [], + accessors: layer.metric ? [{ columnId: layer.metric }] : [], supportsMoreColumns: !layer.metric, filterOperations: numberMetricOperations, required: true, @@ -168,7 +179,7 @@ export const getPieVisualization = ({ defaultMessage: 'Size by', }), layerId, - accessors: layer.metric ? [layer.metric] : [], + accessors: layer.metric ? [{ columnId: layer.metric }] : [], supportsMoreColumns: !layer.metric, filterOperations: numberMetricOperations, required: true, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index b8bceb5454bc8..225fedb987c76 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -242,7 +242,6 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { dragDropContext: DragContextState; - onClick: () => void; }; export interface DatasourceLayerPanelProps { @@ -341,12 +340,19 @@ export type VisualizationDimensionEditorProps = VisualizationConfig setState: (newState: T) => void; }; +export interface AccessorConfig { + columnId: string; + triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none'; + color?: string; + palette?: string[]; +} + export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; /** ID is passed back to visualization. For example, `x` */ groupId: string; - accessors: string[]; + accessors: AccessorConfig[]; supportsMoreColumns: boolean; /** If required, a warning will appear if accessors are empty */ required?: boolean; diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts index b59e09e8c1976..666b0d5098218 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts @@ -128,6 +128,31 @@ describe('color_assignment', () => { expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); expect(formatMock).toHaveBeenCalledWith(complexObject); }); + + it('should handle missing tables', () => { + const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory); + // if there is no data, just assume a single split + expect(assignments.palette1.totalSeriesCount).toEqual(2); + }); + + it('should handle missing columns', () => { + const assignments = getColorAssignments( + layers, + { + ...data, + tables: { + ...data.tables, + '1': { + ...data.tables['1'], + columns: [], + }, + }, + }, + formatFactory + ); + // if the split column is missing, just assume a single split + expect(assignments.palette1.totalSeriesCount).toEqual(2); + }); }); describe('getRank', () => { @@ -178,5 +203,30 @@ describe('color_assignment', () => { // 3 series in front of (complex object)/y1 - abc/y1, abc/y2 expect(assignments.palette1.getRank(layers[0], 'formatted', 'y1')).toEqual(2); }); + + it('should handle missing tables', () => { + const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory); + // if there is no data, assume it is the first splitted series. One series in front - 0/y1 + expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); + }); + + it('should handle missing columns', () => { + const assignments = getColorAssignments( + layers, + { + ...data, + tables: { + ...data.tables, + '1': { + ...data.tables['1'], + columns: [], + }, + }, + }, + formatFactory + ); + // if the split column is missing, assume it is the first splitted series. One series in front - 0/y1 + expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index 5f72dd1b0453b..68c47e11acfc0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -5,20 +5,36 @@ */ import { uniq, mapValues } from 'lodash'; -import { FormatFactory, LensMultiTable } from '../types'; -import { LayerArgs, LayerConfig } from './types'; +import { PaletteOutput } from 'src/plugins/charts/public'; +import { Datatable } from 'src/plugins/expressions'; +import { FormatFactory } from '../types'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; +interface LayerColorConfig { + palette?: PaletteOutput; + splitAccessor?: string; + accessors: string[]; + layerId: string; +} + +export type ColorAssignments = Record< + string, + { + totalSeriesCount: number; + getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string): number; + } +>; + export function getColorAssignments( - layers: LayerArgs[], - data: LensMultiTable, + layers: LayerColorConfig[], + data: { tables: Record }, formatFactory: FormatFactory -) { - const layersPerPalette: Record = {}; +): ColorAssignments { + const layersPerPalette: Record = {}; layers.forEach((layer) => { - const palette = layer.palette?.name || 'palette'; + const palette = layer.palette?.name || 'default'; if (!layersPerPalette[palette]) { layersPerPalette[palette] = []; } @@ -31,18 +47,21 @@ export function getColorAssignments( return { numberOfSeries: layer.accessors.length, splits: [] }; } const splitAccessor = layer.splitAccessor; - const column = data.tables[layer.layerId].columns.find(({ id }) => id === splitAccessor)!; - const splits = uniq( - data.tables[layer.layerId].rows.map((row) => { - let value = row[splitAccessor]; - if (value && !isPrimitive(value)) { - value = formatFactory(column.meta.params).convert(value); - } else { - value = String(value); - } - return value; - }) - ); + const column = data.tables[layer.layerId]?.columns.find(({ id }) => id === splitAccessor); + const splits = + !column || !data.tables[layer.layerId] + ? [] + : uniq( + data.tables[layer.layerId].rows.map((row) => { + let value = row[splitAccessor]; + if (value && !isPrimitive(value)) { + value = formatFactory(column.meta.params).convert(value); + } else { + value = String(value); + } + return value; + }) + ); return { numberOfSeries: (splits.length || 1) * layer.accessors.length, splits }; }); const totalSeriesCount = seriesPerLayer.reduce( @@ -51,18 +70,17 @@ export function getColorAssignments( ); return { totalSeriesCount, - getRank(layer: LayerArgs, seriesKey: string, yAccessor: string) { + getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string) { const layerIndex = paletteLayers.indexOf(layer); const currentSeriesPerLayer = seriesPerLayer[layerIndex]; + const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); return ( (layerIndex === 0 ? 0 : seriesPerLayer .slice(0, layerIndex) .reduce((sum, perLayer) => sum + perLayer.numberOfSeries, 0)) + - (layer.splitAccessor - ? currentSeriesPerLayer.splits.indexOf(seriesKey) * layer.accessors.length - : 0) + + (layer.splitAccessor && splitRank !== -1 ? splitRank * layer.accessors.length : 0) + layer.accessors.indexOf(yAccessor) ); }, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 1bcae4d09e7e7..a4c1e1bd4ba16 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -1386,13 +1386,13 @@ describe('xy_expression', () => { yAccessor: 'a', seriesKeys: ['a'], }) - ).toEqual('black'); + ).toEqual('blue'); expect( (component.find(LineSeries).at(1).prop('color') as Function)!({ yAccessor: 'c', seriesKeys: ['c'], }) - ).toEqual('black'); + ).toEqual('blue'); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 4891a51b3124b..5e5eef2f01c17 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -10,6 +10,7 @@ import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public' import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { EditorFrameSetup, FormatFactory } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import { LensPluginStartDependencies } from '../plugin'; export interface XyVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; @@ -31,7 +32,7 @@ export class XyVisualization { constructor() {} setup( - core: CoreSetup, + core: CoreSetup, { expressions, formatFactory, editorFrame, charts }: XyVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { @@ -46,6 +47,7 @@ export class XyVisualization { getXyChartRenderer, getXyVisualization, } = await import('../async_services'); + const [, { data }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => yAxisConfig); @@ -64,7 +66,7 @@ export class XyVisualization { histogramBarTarget: core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), }) ); - return getXyVisualization({ paletteService: palettes }); + return getXyVisualization({ paletteService: palettes, data }); }); } } diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index bf4ffaa36a870..bd479062e2a06 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -5,7 +5,7 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { FramePublicAPI } from '../types'; +import { FramePublicAPI, DatasourcePublicAPI } from '../types'; import { SeriesType, visualizationTypes, LayerConfig, YConfig, ValidLayer } from './types'; export function isHorizontalSeries(seriesType: SeriesType) { @@ -39,6 +39,18 @@ export const getSeriesColor = (layer: LayerConfig, accessor: string) => { ); }; +export const getColumnToLabelMap = (layer: LayerConfig, datasource: DatasourcePublicAPI) => { + const columnToLabel: Record = {}; + + layer.accessors.concat(layer.splitAccessor ? [layer.splitAccessor] : []).forEach((accessor) => { + const operation = datasource.getOperationForColumnId(accessor); + if (operation?.label) { + columnToLabel[accessor] = operation.label; + } + }); + return columnToLabel; +}; + export function hasHistogramSeries( layers: ValidLayer[] = [], datasourceLayers?: FramePublicAPI['datasourceLayers'] diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 05a4b7f460adb..a715e4359da47 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -10,10 +10,12 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks' import { getXyVisualization } from './xy_visualization'; import { Operation } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), + data: dataPluginMock.createStartContract(), }); let mockDatasource: ReturnType; let frame: ReturnType; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index df773146cde4d..fda7c93af03a5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -9,6 +9,7 @@ import { ScaleType } from '@elastic/charts'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { State, ValidLayer, LayerConfig } from './types'; import { OperationMetadata, DatasourcePublicAPI } from '../types'; +import { getColumnToLabelMap } from './state_helpers'; export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: LayerConfig) => { const originalOrder = datasource @@ -196,17 +197,7 @@ export const buildExpression = ( ], valueLabels: [state?.valueLabels || 'hide'], layers: validLayers.map((layer) => { - const columnToLabel: Record = {}; - - const datasource = datasourceLayers[layer.layerId]; - layer.accessors - .concat(layer.splitAccessor ? [layer.splitAccessor] : []) - .forEach((accessor) => { - const operation = datasource.getOperationForColumnId(accessor); - if (operation?.label) { - columnToLabel[accessor] = operation.label; - } - }); + const columnToLabel = getColumnToLabelMap(layer, datasourceLayers[layer.layerId]); const xAxisOperation = datasourceLayers && diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 5127e5c2c2597..546cf06d4014e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -7,10 +7,11 @@ import { getXyVisualization } from './visualization'; import { Position } from '@elastic/charts'; import { Operation } from '../types'; -import { State, SeriesType } from './types'; +import { State, SeriesType, LayerConfig } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { LensIconChartBar } from '../assets/chart_bar'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; function exampleState(): State { return { @@ -28,9 +29,12 @@ function exampleState(): State { ], }; } +const paletteServiceMock = chartPluginMock.createPaletteRegistry(); +const dataMock = dataPluginMock.createStartContract(); const xyVisualization = getXyVisualization({ - paletteService: chartPluginMock.createPaletteRegistry(), + paletteService: paletteServiceMock, + data: dataMock, }); describe('xy_visualization', () => { @@ -307,6 +311,14 @@ describe('xy_visualization', () => { frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; + + frame.activeData = { + first: { + type: 'datatable', + rows: [], + columns: [], + }, + }; }); it('should return options for 3 dimensions', () => { @@ -408,6 +420,145 @@ describe('xy_visualization', () => { ]; expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); + + describe('color assignment', () => { + function callConfig(layerConfigOverride: Partial) { + const baseState = exampleState(); + const options = xyVisualization.getConfiguration({ + state: { + ...baseState, + layers: [ + { + ...baseState.layers[0], + splitAccessor: undefined, + ...layerConfigOverride, + }, + ], + }, + frame, + layerId: 'first', + }).groups; + return options; + } + + function callConfigForYConfigs(layerConfigOverride: Partial) { + return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'y'); + } + + function callConfigForBreakdownConfigs(layerConfigOverride: Partial) { + return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'breakdown'); + } + + function callConfigAndFindYConfig( + layerConfigOverride: Partial, + assertionAccessor: string + ) { + const accessorConfig = callConfigForYConfigs(layerConfigOverride)?.accessors.find( + (accessor) => typeof accessor !== 'string' && accessor.columnId === assertionAccessor + ); + if (!accessorConfig || typeof accessorConfig === 'string') { + throw new Error('could not find accessor'); + } + return accessorConfig; + } + + it('should pass custom y color in accessor config', () => { + const accessorConfig = callConfigAndFindYConfig( + { + yConfig: [ + { + forAccessor: 'b', + color: 'red', + }, + ], + }, + 'b' + ); + expect(accessorConfig.triggerIcon).toEqual('color'); + expect(accessorConfig.color).toEqual('red'); + }); + + it('should query palette to fill in colors for other dimensions', () => { + const palette = paletteServiceMock.get('default'); + (palette.getColor as jest.Mock).mockClear(); + const accessorConfig = callConfigAndFindYConfig({}, 'c'); + expect(accessorConfig.triggerIcon).toEqual('color'); + // black is the color returned from the palette mock + expect(accessorConfig.color).toEqual('black'); + expect(palette.getColor).toHaveBeenCalledWith( + [ + { + name: 'c', + // rank 1 because it's the second y metric + rankAtDepth: 1, + totalSeriesAtDepth: 2, + }, + ], + { maxDepth: 1, totalSeries: 2 }, + undefined + ); + }); + + it('should pass name of current series along', () => { + (frame.datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({ + label: 'Overwritten label', + }); + const palette = paletteServiceMock.get('default'); + (palette.getColor as jest.Mock).mockClear(); + callConfigAndFindYConfig({}, 'c'); + expect(palette.getColor).toHaveBeenCalledWith( + [ + expect.objectContaining({ + name: 'Overwritten label', + }), + ], + expect.anything(), + undefined + ); + }); + + it('should use custom palette if layer contains palette', () => { + const palette = paletteServiceMock.get('mock'); + callConfigAndFindYConfig( + { + palette: { type: 'palette', name: 'mock', params: {} }, + }, + 'c' + ); + expect(palette.getColor).toHaveBeenCalled(); + }); + + it('should not show any indicator as long as there is no data', () => { + frame.activeData = undefined; + const yConfigs = callConfigForYConfigs({}); + expect(yConfigs!.accessors.length).toEqual(2); + yConfigs!.accessors.forEach((accessor) => { + expect(accessor.triggerIcon).toBeUndefined(); + }); + }); + + it('should show disable icon for splitted series', () => { + const accessorConfig = callConfigAndFindYConfig( + { + splitAccessor: 'd', + }, + 'b' + ); + expect(accessorConfig.triggerIcon).toEqual('disabled'); + }); + + it('should show current palette for break down by dimension', () => { + const palette = paletteServiceMock.get('mock'); + const customColors = ['yellow', 'green']; + (palette.getColors as jest.Mock).mockReturnValue(customColors); + const breakdownConfig = callConfigForBreakdownConfigs({ + palette: { type: 'palette', name: 'mock', params: {} }, + splitAccessor: 'd', + }); + const accessorConfig = breakdownConfig!.accessors[0]; + expect(typeof accessorConfig !== 'string' && accessorConfig.palette).toEqual(customColors); + }); + }); }); describe('#getErrorMessages', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 1f135929dac21..5748e649c181e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -10,16 +10,24 @@ import { render } from 'react-dom'; import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { PaletteRegistry } from 'src/plugins/charts/public'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; -import { Visualization, OperationMetadata, VisualizationType } from '../types'; +import { + Visualization, + OperationMetadata, + VisualizationType, + AccessorConfig, + FramePublicAPI, +} from '../types'; import { State, SeriesType, visualizationTypes, LayerConfig } from './types'; -import { isHorizontalChart } from './state_helpers'; +import { getColumnToLabelMap, isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; +import { ColorAssignments, getColorAssignments } from './color_assignment'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -76,8 +84,10 @@ function getDescription(state?: State) { export const getXyVisualization = ({ paletteService, + data, }: { paletteService: PaletteRegistry; + data: DataPublicPluginStart; }): Visualization => ({ id: 'lnsXY', @@ -168,7 +178,25 @@ export const getXyVisualization = ({ const datasource = frame.datasourceLayers[layer.layerId]; - const sortedAccessors = getSortedAccessors(datasource, layer); + const sortedAccessors: string[] = getSortedAccessors(datasource, layer); + let mappedAccessors: AccessorConfig[] = sortedAccessors.map((accessor) => ({ + columnId: accessor, + })); + + if (frame.activeData) { + const colorAssignments = getColorAssignments( + state.layers, + { tables: frame.activeData }, + data.fieldFormats.deserialize + ); + mappedAccessors = getAccessorColorConfig( + colorAssignments, + frame, + layer, + sortedAccessors, + paletteService + ); + } const isHorizontal = isHorizontalChart(state.layers); return { @@ -176,7 +204,7 @@ export const getXyVisualization = ({ { groupId: 'x', groupLabel: getAxisName('x', { isHorizontal }), - accessors: layer.xAccessor ? [layer.xAccessor] : [], + accessors: layer.xAccessor ? [{ columnId: layer.xAccessor }] : [], filterOperations: isBucketed, supportsMoreColumns: !layer.xAccessor, dataTestSubj: 'lnsXY_xDimensionPanel', @@ -184,7 +212,7 @@ export const getXyVisualization = ({ { groupId: 'y', groupLabel: getAxisName('y', { isHorizontal }), - accessors: sortedAccessors, + accessors: mappedAccessors, filterOperations: isNumericMetric, supportsMoreColumns: true, required: true, @@ -196,7 +224,17 @@ export const getXyVisualization = ({ groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { defaultMessage: 'Break down by', }), - accessors: layer.splitAccessor ? [layer.splitAccessor] : [], + accessors: layer.splitAccessor + ? [ + { + columnId: layer.splitAccessor, + triggerIcon: 'colorBy', + palette: paletteService + .get(layer.palette?.name || 'default') + .getColors(10, layer.palette?.params), + }, + ] + : [], filterOperations: isBucketed, supportsMoreColumns: !layer.splitAccessor, dataTestSubj: 'lnsXY_splitDimensionPanel', @@ -333,6 +371,51 @@ export const getXyVisualization = ({ }, }); +function getAccessorColorConfig( + colorAssignments: ColorAssignments, + frame: FramePublicAPI, + layer: LayerConfig, + sortedAccessors: string[], + paletteService: PaletteRegistry +): AccessorConfig[] { + const layerContainsSplits = Boolean(layer.splitAccessor); + const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; + const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; + return sortedAccessors.map((accessor) => { + const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); + if (layerContainsSplits) { + return { + columnId: accessor as string, + triggerIcon: 'disabled', + }; + } + const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); + const rank = colorAssignments[currentPalette.name].getRank( + layer, + columnToLabel[accessor] || accessor, + accessor + ); + const customColor = + currentYConfig?.color || + paletteService.get(currentPalette.name).getColor( + [ + { + name: columnToLabel[accessor] || accessor, + rankAtDepth: rank, + totalSeriesAtDepth: totalSeriesCount, + }, + ], + { maxDepth: 1, totalSeries: totalSeriesCount }, + currentPalette.params + ); + return { + columnId: accessor as string, + triggerIcon: customColor ? 'color' : 'disabled', + color: customColor ? customColor : undefined, + }; + }); +} + function validateLayersForDimension( dimension: string, layers: LayerConfig[], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index e8c3282146097..d214554de340c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -15,12 +15,14 @@ import { State, XYState, visualizationTypes } from './types'; import { generateId } from '../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { PaletteOutput } from 'src/plugins/charts/public'; jest.mock('../id_generator'); const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), + data: dataPluginMock.createStartContract(), }); describe('xy_suggestions', () => { diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 8983d4ab1a4da..e47968b027cc3 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -19,7 +19,7 @@ "savedObjects", "share" ], - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "savedObjectsTagging"], "ui": true, "server": true, "extraPublicDirs": ["common/constants"], diff --git a/x-pack/plugins/maps/public/classes/layers/_layers.scss b/x-pack/plugins/maps/public/classes/layers/_layers.scss index 54ab7d85ef170..f3685b163e397 100644 --- a/x-pack/plugins/maps/public/classes/layers/_layers.scss +++ b/x-pack/plugins/maps/public/classes/layers/_layers.scss @@ -6,6 +6,10 @@ } &__background { - fill: $euiColorLightShade; + fill: lightOrDarkTheme($euiColorLightShade, $euiColorMediumShade); + } + + &__backgroundDarker { + fill: lightOrDarkTheme($euiColorMediumShade, $euiColorDarkShade); } } diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx index d87302a6a9f2e..670c775c3cc92 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { LayerWizard, RenderWizardArguments } from '../layer_wizard_registry'; import { LayerTemplate } from './layer_template'; -import { ChoroplethLayerIcon } from './cloropleth_layer_icon'; +import { ChoroplethLayerIcon } from '../icons/cloropleth_layer_icon'; export const choroplethLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/cloropleth_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/cloropleth_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/cloropleth_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/cloropleth_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/clusters_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/clusters_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/documents_layer_icon.tsx similarity index 98% rename from x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/documents_layer_icon.tsx index dcd4985f44280..168c8f8072b32 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx +++ b/x-pack/plugins/maps/public/classes/layers/icons/documents_layer_icon.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent } from 'react'; -export const EsDocumentsLayerIcon: FunctionComponent = () => ( +export const DocumentsLayerIcon: FunctionComponent = () => ( ( + + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/heatmap_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/heatmap_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/point_2_point_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/point_2_point_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/layers/icons/tracks_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/tracks_layer_icon.tsx new file mode 100644 index 0000000000000..5070e49ddc2b6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/tracks_layer_icon.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const TracksLayerIcon: FunctionComponent = () => ( + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/icons/vector_tile_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/vector_tile_layer_icon.tsx new file mode 100644 index 0000000000000..8c4cd11221622 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/vector_tile_layer_icon.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const VectorTileLayerIcon: FunctionComponent = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/icons/web_map_service_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/web_map_service_layer_icon.tsx new file mode 100644 index 0000000000000..d845a66b93748 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/web_map_service_layer_icon.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const WebMapServiceLayerIcon: FunctionComponent = () => ( + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/icons/world_map_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/world_map_layer_icon.tsx new file mode 100644 index 0000000000000..702371e52996f --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/world_map_layer_icon.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const WorldMapLayerIcon: FunctionComponent = () => ( + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index 768bbd1d94700..78100fe501208 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -14,6 +14,7 @@ import { EMSFileSource, sourceTitle } from './ems_file_source'; import { getEMSSettings } from '../../../kibana_services'; import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { EMSBoundariesLayerIcon } from '../../layers/icons/ems_boundaries_layer_icon'; export const emsBoundariesLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], @@ -24,7 +25,7 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), - icon: 'emsApp', + icon: EMSBoundariesLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index bfa46574f007a..ff65889e15d27 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -15,6 +15,7 @@ import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_laye import { TileServiceSelect } from './tile_service_select'; import { getEMSSettings } from '../../../kibana_services'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { WorldMapLayerIcon } from '../../layers/icons/world_map_layer_icon'; export const emsBaseMapLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], @@ -25,7 +26,7 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', }), - icon: 'emsApp', + icon: WorldMapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { const layerDescriptor = VectorTileLayer.createDescriptor({ diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 5d0a414cd0d18..0f596c47fc9b6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -29,7 +29,7 @@ import { STYLE_TYPE, } from '../../../../common/constants'; import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; -import { ClustersLayerIcon } from './clusters_layer_icon'; +import { ClustersLayerIcon } from '../../layers/icons/clusters_layer_icon'; export const clustersLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index 652514a3b9d34..dcad1e1e0b4b9 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -15,7 +15,7 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re import { HeatmapLayer } from '../../layers/heatmap_layer/heatmap_layer'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../common/constants'; -import { HeatmapLayerIcon } from './heatmap_layer_icon'; +import { HeatmapLayerIcon } from '../../layers/icons/heatmap_layer_icon'; export const heatmapLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index 74e690d4d3204..9d3ccf128fe05 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -23,7 +23,7 @@ import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types'; -import { Point2PointLayerIcon } from './point_2_point_layer_icon'; +import { Point2PointLayerIcon } from '../../layers/icons/point_2_point_layer_icon'; export const point2PointLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 80cc88f432cad..04671b931d56b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -15,7 +15,7 @@ import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_ve import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; -import { EsDocumentsLayerIcon } from './es_documents_layer_icon'; +import { DocumentsLayerIcon } from '../../layers/icons/documents_layer_icon'; import { ESSearchSourceDescriptor, VectorLayerDescriptor, @@ -41,7 +41,7 @@ export const esDocumentsLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.esSearchDescription', { defaultMessage: 'Points, lines, and polygons from Elasticsearch', }), - icon: EsDocumentsLayerIcon, + icon: DocumentsLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index 32fa329be85df..db05570af05d5 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -12,13 +12,14 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types'; +import { VectorTileLayerIcon } from '../../layers/icons/vector_tile_layer_icon'; export const mvtVectorSourceWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { defaultMessage: 'Data service implementing the Mapbox vector tile specification', }), - icon: 'grid', + icon: VectorTileLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index b3950baf8dbeb..47a426e34a420 100644 --- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -13,13 +13,14 @@ import { sourceTitle, WMSSource } from './wms_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { TileLayer } from '../../layers/tile_layer/tile_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { WebMapServiceLayerIcon } from '../../layers/icons/web_map_service_layer_icon'; export const wmsLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.wmsDescription', { defaultMessage: 'Maps from OGC Standard WMS', }), - icon: 'grid', + icon: WebMapServiceLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index b0344a3e0e318..e442907d172e9 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -11,13 +11,14 @@ import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { TileLayer } from '../../layers/tile_layer/tile_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { WorldMapLayerIcon } from '../../layers/icons/world_map_layer_icon'; export const tmsLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.ems_xyzDescription', { defaultMessage: 'Tile map service configured in interface', }), - icon: 'grid', + icon: WorldMapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig | null) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 4dcc9193420c0..02b875257a5ac 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -48,6 +48,7 @@ export const getCoreI18n = () => coreStart.i18n; export const getSearchService = () => pluginsStart.data.search; export const getEmbeddableService = () => pluginsStart.embeddable; export const getNavigateToApp = () => coreStart.application.navigateToApp; +export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; // xpack.maps.* kibana.yml settings from this plugin let mapAppConfig: MapsConfigType; diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 0e3ef1b9ea518..9b2f3105f6870 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectReference } from 'src/core/types'; import { AttributeService } from '../../../../src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; @@ -14,11 +15,9 @@ import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './ import { extractReferences, injectReferences } from '../common/migrations/references'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; -export type MapAttributeService = AttributeService< - MapSavedObjectAttributes, - MapByValueInput, - MapByReferenceInput ->; +type MapDoc = MapSavedObjectAttributes & { references?: SavedObjectReference[] }; + +export type MapAttributeService = AttributeService; let mapAttributeService: MapAttributeService | null = null; @@ -28,30 +27,37 @@ export function getMapAttributeService(): MapAttributeService { } mapAttributeService = getEmbeddableService().getAttributeService< - MapSavedObjectAttributes, + MapDoc, MapByValueInput, MapByReferenceInput >(MAP_SAVED_OBJECT_TYPE, { - saveMethod: async (attributes: MapSavedObjectAttributes, savedObjectId?: string) => { - const { attributes: attributesWithExtractedReferences, references } = extractReferences({ - attributes, + saveMethod: async (attributes: MapDoc, savedObjectId?: string) => { + // AttributeService "attributes" contains "references" as a child. + // SavedObjectClient "attributes" uses "references" as a sibling. + // https://github.com/elastic/kibana/issues/83133 + const savedObjectClientReferences = attributes.references; + const savedObjectClientAttributes = { ...attributes }; + delete savedObjectClientAttributes.references; + const { attributes: updatedAttributes, references } = extractReferences({ + attributes: savedObjectClientAttributes, + references: savedObjectClientReferences, }); const savedObject = await (savedObjectId ? getSavedObjectsClient().update( MAP_SAVED_OBJECT_TYPE, savedObjectId, - attributesWithExtractedReferences, + updatedAttributes, { references } ) : getSavedObjectsClient().create( MAP_SAVED_OBJECT_TYPE, - attributesWithExtractedReferences, + updatedAttributes, { references } )); return { id: savedObject.id }; }, - unwrapMethod: async (savedObjectId: string): Promise => { + unwrapMethod: async (savedObjectId: string): Promise => { const savedObject = await getSavedObjectsClient().get( MAP_SAVED_OBJECT_TYPE, savedObjectId @@ -62,7 +68,7 @@ export function getMapAttributeService(): MapAttributeService { } const { attributes } = injectReferences(savedObject); - return attributes; + return { ...attributes, references: savedObject.references }; }, checkForDuplicateTitle: (props: OnSaveProps) => { return checkForDuplicateTitle( diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 3da346aaf4443..ecb647cbb61b2 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -63,6 +63,7 @@ import { setLicensingPluginStart, } from './licensed_features'; import { EMSSettings } from '../common/ems_settings'; +import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -86,6 +87,7 @@ export interface MapsPluginStartDependencies { visualizations: VisualizationsStart; savedObjects: SavedObjectsStart; dashboard: DashboardStart; + savedObjectsTagging?: SavedObjectTaggingPluginStart; } /** diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index 97ed3d428d341..a579e3f122cc6 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -5,45 +5,68 @@ */ import React, { MouseEvent } from 'react'; -import _ from 'lodash'; -import { - EuiTitle, - EuiFieldSearch, - EuiBasicTable, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiSpacer, - EuiOverlayMask, - EuiConfirmModal, - EuiCallOut, -} from '@elastic/eui'; +import { SavedObjectReference } from 'src/core/types'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Direction } from '@elastic/eui'; -import { - CriteriaWithPagination, - EuiBasicTableColumn, -} from '@elastic/eui/src/components/basic_table/basic_table'; -import { EuiTableSortingType } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import { TableListView } from '../../../../../../src/plugins/kibana_react/public'; import { goToSpecifiedPath } from '../../render_app'; import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { getMapsCapabilities, - getUiSettings, getToasts, getCoreChrome, getNavigateToApp, getSavedObjectsClient, + getSavedObjectsTagging, + getSavedObjects, } from '../../kibana_services'; import { getAppTitle } from '../../../common/i18n_getters'; import { MapSavedObjectAttributes } from '../../../common/map_saved_object_type'; -export const EMPTY_FILTER = ''; +interface MapItem { + id: string; + title: string; + description?: string; + references?: SavedObjectReference[]; +} + +const savedObjectsTagging = getSavedObjectsTagging(); +const searchFilters = savedObjectsTagging + ? [savedObjectsTagging.ui.getSearchBarFilter({ useName: true })] + : []; + +const tableColumns: Array> = [ + { + field: 'title', + name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', { + defaultMessage: 'Title', + }), + sortable: true, + render: (field: string, record: MapItem) => ( + { + e.preventDefault(); + goToSpecifiedPath(`/${MAP_PATH}/${record.id}`); + }} + data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} + > + {field} + + ), + }, + { + field: 'description', + name: i18n.translate('xpack.maps.mapListing.descriptionFieldTitle', { + defaultMessage: 'Description', + }), + dataType: 'string', + sortable: true, + }, +]; +if (savedObjectsTagging) { + tableColumns.push(savedObjectsTagging.ui.getTableColumnDefinition()); +} function navigateToNewMap() { const navigateToApp = getNavigateToApp(); @@ -51,447 +74,76 @@ function navigateToNewMap() { path: MAP_PATH, }); } -interface State { - sortField?: string | number | symbol; - sortDirection?: Direction; - hasInitialFetchReturned: boolean; - isFetchingItems: boolean; - showDeleteModal: boolean; - showLimitError: boolean; - filter: string; - items: TableRow[]; - selectedIds: string[]; - page: number; - perPage: number; - readOnly: boolean; - listingLimit: number; - totalItems?: number; -} - -interface TableRow { - id: string; - title: string; - description?: string; -} - -export class MapsListView extends React.Component { - _isMounted: boolean = false; - state: State = { - hasInitialFetchReturned: false, - isFetchingItems: false, - showDeleteModal: false, - showLimitError: false, - filter: EMPTY_FILTER, - items: [], - selectedIds: [], - page: 0, - perPage: 20, - readOnly: !getMapsCapabilities().save, - listingLimit: getUiSettings().get('savedObjects:listingLimit'), - }; - componentWillUnmount() { - this._isMounted = false; - this.debouncedFetch.cancel(); - } +async function findMaps(searchQuery: string) { + let searchTerm = searchQuery; + let tagReferences; - componentDidMount() { - this._isMounted = true; - this.initMapList(); - } - - async initMapList() { - this.fetchItems(); - getCoreChrome().docTitle.change(getAppTitle()); - getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); - } - - debouncedFetch = _.debounce(async (filter) => { - const response = await getSavedObjectsClient().find({ - type: MAP_SAVED_OBJECT_TYPE, - search: filter ? `${filter}*` : undefined, - perPage: this.state.listingLimit, - page: 1, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - fields: ['description', 'title'], - }); - - if (!this._isMounted) { - return; - } - - // We need this check to handle the case where search results come back in a different - // order than they were sent out. Only load results for the most recent search. - if (filter === this.state.filter) { - this.setState({ - hasInitialFetchReturned: true, - isFetchingItems: false, - items: response.savedObjects.map((savedObject) => { - return { - id: savedObject.id, - title: savedObject.attributes.title, - description: savedObject.attributes.description, - }; - }), - totalItems: response.total, - showLimitError: response.total > this.state.listingLimit, - }); - } - }, 300); - - fetchItems = () => { - this.setState( - { - isFetchingItems: true, - }, - this.debouncedFetch.bind(null, this.state.filter) - ); - }; - - deleteSelectedItems = async () => { - try { - const deletions = this.state.selectedIds.map((id) => { - return getSavedObjectsClient().delete(MAP_SAVED_OBJECT_TYPE, id); - }); - await Promise.all(deletions); - } catch (error) { - getToasts().addDanger({ - title: i18n.translate('xpack.maps.mapListing.unableToDeleteToastTitle', { - defaultMessage: `Unable to delete map(s)`, - }), - text: `${error}`, - }); - } - this.fetchItems(); - this.setState({ - selectedIds: [], + if (savedObjectsTagging) { + const parsed = savedObjectsTagging.ui.parseSearchQuery(searchQuery, { + useName: true, }); - this.closeDeleteModal(); - }; - - closeDeleteModal = () => { - this.setState({ showDeleteModal: false }); - }; - - openDeleteModal = () => { - this.setState({ showDeleteModal: true }); - }; - - onTableChange = ({ page, sort }: CriteriaWithPagination) => { - const { index: pageIndex, size: pageSize } = page; - - let { field: sortField, direction: sortDirection } = sort || {}; - - // 3rd sorting state that is not captured by sort - native order (no sort) - // when switching from desc to asc for the same field - use native order - if ( - this.state.sortField === sortField && - this.state.sortDirection === 'desc' && - sortDirection === 'asc' - ) { - sortField = undefined; - sortDirection = undefined; - } - - this.setState({ - page: pageIndex, - perPage: pageSize, - sortField, - sortDirection, - }); - }; - - getPageOfItems = () => { - // do not sort original list to preserve elasticsearch ranking order - const itemsCopy = this.state.items.slice(); - - if (this.state.sortField) { - itemsCopy.sort((a, b) => { - const fieldA = _.get(a, this.state.sortField!, ''); - const fieldB = _.get(b, this.state.sortField!, ''); - let order = 1; - if (this.state.sortDirection === 'desc') { - order = -1; - } - return order * fieldA.toLowerCase().localeCompare(fieldB.toLowerCase()); - }); - } - - // If begin is greater than the length of the sequence, an empty array is returned. - const startIndex = this.state.page * this.state.perPage; - // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). - const lastIndex = startIndex + this.state.perPage; - return itemsCopy.slice(startIndex, lastIndex); - }; - - hasNoItems() { - if (!this.state.isFetchingItems && this.state.items.length === 0 && !this.state.filter) { - return true; - } - - return false; + searchTerm = parsed.searchTerm; + tagReferences = parsed.tagReferences; } - renderConfirmDeleteModal() { - return ( - - -

- -

- - - ); - } - - renderListingLimitWarning() { - if (this.state.showLimitError) { - return ( - - -

- - - - - . -

-
- -
- ); - } - } - - renderNoResultsMessage() { - if (this.state.isFetchingItems) { - return ''; - } - - if (this.hasNoItems()) { - return i18n.translate('xpack.maps.mapListing.noItemsDescription', { - defaultMessage: `Looks like you don't have any maps. Click the create button to create one.`, - }); - } - - return i18n.translate('xpack.maps.mapListing.noMatchDescription', { - defaultMessage: 'No items matched your search.', - }); - } - - renderSearchBar() { - let deleteBtn; - if (this.state.selectedIds.length > 0) { - deleteBtn = ( - - - - - - ); - } - - return ( - - {deleteBtn} - - { - this.setState( - { - filter: e.target.value, - }, - this.fetchItems - ); - }} - data-test-subj="searchFilter" - /> - - - ); - } - - renderTable() { - const tableColumns: Array> = [ - { - field: 'title', - name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', { - defaultMessage: 'Title', - }), - sortable: true, - render: (field, record) => ( - { - e.preventDefault(); - goToSpecifiedPath(`/${MAP_PATH}/${record.id}`); - }} - data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} - > - {field} - - ), - }, - { - field: 'description', - name: i18n.translate('xpack.maps.mapListing.descriptionFieldTitle', { - defaultMessage: 'Description', - }), - dataType: 'string', - sortable: true, - }, - ]; - const pagination = { - pageIndex: this.state.page, - pageSize: this.state.perPage, - totalItemCount: this.state.items.length, - pageSizeOptions: [10, 20, 50], - }; - - let selection; - if (!this.state.readOnly) { - selection = { - onSelectionChange: (s: TableRow[]) => { - this.setState({ - selectedIds: s.map((item) => { - return item.id; - }), - }); - }, - }; - } + const resp = await getSavedObjectsClient().find({ + type: MAP_SAVED_OBJECT_TYPE, + search: searchTerm ? `${searchTerm}*` : undefined, + perPage: getSavedObjects().settings.getListingLimit(), + page: 1, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + fields: ['description', 'title'], + hasReference: tagReferences, + }); - const sorting: EuiTableSortingType = {}; - if (this.state.sortField) { - sorting.sort = { - field: this.state.sortField, - direction: this.state.sortDirection!, + return { + total: resp.total, + hits: resp.savedObjects.map((savedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + description: savedObject.attributes.description, + references: savedObject.references, }; - } - const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); - - return ( - - ); - } - - renderListing() { - let createButton; - if (!this.state.readOnly) { - createButton = ( - - - - ); - } - return ( - - {this.state.showDeleteModal && this.renderConfirmDeleteModal()} - - - - -

- -

-
-
- - {createButton} -
- - - - {this.renderListingLimitWarning()} - - {this.renderSearchBar()} - - - - {this.renderTable()} -
- ); - } - - renderPageContent() { - if (!this.state.hasInitialFetchReturned) { - return; - } + }), + }; +} - return {this.renderListing()}; - } +async function deleteMaps(items: object[]) { + const deletions = items.map((item) => { + return getSavedObjectsClient().delete(MAP_SAVED_OBJECT_TYPE, (item as MapItem).id); + }); + await Promise.all(deletions); +} - render() { - return ( - - {this.renderPageContent()} - - ); - } +export function MapsListView() { + const isReadOnly = !getMapsCapabilities().save; + + getCoreChrome().docTitle.change(getAppTitle()); + getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); + + return ( + + ); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 036f8cf11d374..98f428f9a2999 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -33,7 +33,12 @@ import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_sele import { getMapAttributeService } from '../../../map_attribute_service'; import { OnSaveProps } from '../../../../../../../src/plugins/saved_objects/public'; import { MapByReferenceInput, MapEmbeddableInput } from '../../../embeddable/types'; -import { getCoreChrome, getToasts, getIsAllowByValueEmbeddables } from '../../../kibana_services'; +import { + getCoreChrome, + getToasts, + getIsAllowByValueEmbeddables, + getSavedObjectsTagging, +} from '../../../kibana_services'; import { goToSpecifiedPath } from '../../../render_app'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { getInitialLayers } from './get_initial_layers'; @@ -51,6 +56,7 @@ export class SavedMap { private _originatingApp?: string; private readonly _stateTransfer?: EmbeddableStateTransfer; private readonly _store: MapStore; + private _tags: string[] = []; constructor({ defaultLayers = [], @@ -87,7 +93,14 @@ export class SavedMap { description: '', }; } else { - this._attributes = await getMapAttributeService().unwrapAttributes(this._mapEmbeddableInput); + const doc = await getMapAttributeService().unwrapAttributes(this._mapEmbeddableInput); + const references = doc.references; + delete doc.references; + this._attributes = doc; + const savedObjectsTagging = getSavedObjectsTagging(); + if (savedObjectsTagging && references && references.length) { + this._tags = savedObjectsTagging.ui.getTagIdsFromReferences(references); + } } if (this._attributes?.mapStateJSON) { @@ -216,6 +229,10 @@ export class SavedMap { return this._getStateTransfer().getAppNameFromId(appId); }; + public getTags(): string[] { + return this._tags; + } + public hasSaveAndReturnConfig() { const hasOriginatingApp = !!this._originatingApp; const isNewMap = !this.getSavedObjectId(); @@ -247,9 +264,11 @@ export class SavedMap { newTitle, newCopyOnSave, returnToOrigin, + newTags, saveByReference, }: OnSaveProps & { returnToOrigin: boolean; + newTags?: string[]; saveByReference: boolean; }) { if (!this._attributes) { @@ -264,8 +283,17 @@ export class SavedMap { let updatedMapEmbeddableInput: MapEmbeddableInput; try { + const savedObjectsTagging = getSavedObjectsTagging(); + // Attribute service deviates from Saved Object client by including references as a child to attributes in stead of a sibling + const attributes = + savedObjectsTagging && newTags + ? { + ...this._attributes, + references: savedObjectsTagging.ui.updateTagsReferences([], newTags), + } + : this._attributes; updatedMapEmbeddableInput = (await getMapAttributeService().wrapAttributes( - this._attributes, + attributes, saveByReference, newCopyOnSave ? undefined : this._mapEmbeddableInput )) as MapEmbeddableInput; diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 2d0a7d967a6cf..43a74a9c73012 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -14,6 +14,7 @@ import { getCoreI18n, getSavedObjectsClient, getCoreOverlays, + getSavedObjectsTagging, } from '../../kibana_services'; import { checkForDuplicateTitle, @@ -125,6 +126,19 @@ export function getTopNavConfig({ } }, run: () => { + let selectedTags = savedMap.getTags(); + function onTagsSelected(newTags: string[]) { + selectedTags = newTags; + } + + const savedObjectsTagging = getSavedObjectsTagging(); + const tagSelector = savedObjectsTagging ? ( + + ) : undefined; + const saveModal = ( ); showSaveModal(saveModal, getCoreI18n().Context); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 65d79272494f0..a79e5353048c8 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -185,7 +185,7 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], savedObject: { all: [MAP_SAVED_OBJECT_TYPE, 'query'], - read: ['index-pattern'], + read: ['index-pattern', 'tag'], }, ui: ['save', 'show', 'saveQuery'], }, @@ -194,7 +194,7 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], savedObject: { all: [], - read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query'], + read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query', 'tag'], }, ui: ['show'], }, diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 5c8000566bb38..958d5ae250185 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -9,6 +9,7 @@ export const ANALYSIS_CONFIG_TYPE = { REGRESSION: 'regression', CLASSIFICATION: 'classification', } as const; + export const DEFAULT_RESULTS_FIELD = 'ml'; export const JOB_MAP_NODE_TYPES = { diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index a9e4cdc4a0434..1027ee5bf9a89 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -442,6 +442,16 @@ export const getMessages = once(() => { url: 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, + missing_summary_count_field_name: { + status: VALIDATION_STATUS.ERROR, + text: i18n.translate( + 'xpack.ml.models.jobValidation.messages.missingSummaryCountFieldNameMessage', + { + defaultMessage: + 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', + } + ), + }, skipped_extended_tests: { status: VALIDATION_STATUS.WARNING, text: i18n.translate('xpack.ml.models.jobValidation.messages.skippedExtendedTestsMessage', { diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts index a79e72a84c08e..0c931d281d2d5 100644 --- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -11,6 +11,7 @@ export const ML_PAGES = { ANOMALY_EXPLORER: 'explorer', SINGLE_METRIC_VIEWER: 'timeseriesexplorer', DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', + DATA_FRAME_ANALYTICS_MODELS_MANAGE: 'data_frame_analytics/models', DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map', /** @@ -45,3 +46,5 @@ export const ML_PAGES = { ACCESS_DENIED: 'access-denied', OVERVIEW: 'overview', } as const; + +export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES]; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts index 47ff618ffa77f..e5294112dc095 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts @@ -19,7 +19,7 @@ export interface Datafeed { job_id: JobId; query: object; query_delay?: string; - script_fields?: object; + script_fields?: Record; scroll_size?: number; delayed_data_check_config?: object; indices_options?: IndicesOptions; @@ -30,16 +30,17 @@ export interface ChunkingConfig { time_span?: string; } -interface Aggregation { - buckets: { +export type Aggregation = Record< + string, + { date_histogram: { field: string; fixed_interval: string; }; aggregations?: { [key: string]: any }; aggs?: { [key: string]: any }; - }; -} + } +>; interface IndicesOptions { expand_wildcards?: 'all' | 'open' | 'closed' | 'hidden' | 'none'; diff --git a/x-pack/plugins/ml/common/types/common.ts b/x-pack/plugins/ml/common/types/common.ts index f04ff2539e4e9..4ae542c510a26 100644 --- a/x-pack/plugins/ml/common/types/common.ts +++ b/x-pack/plugins/ml/common/types/common.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MlPages } from '../constants/ml_url_generator'; + export interface Dictionary { [id: string]: TValue; } @@ -31,3 +33,15 @@ export type DeepReadonly = T extends Array type DeepReadonlyObject = { readonly [P in keyof T]: DeepReadonly; }; + +export interface ListingPageUrlState { + pageSize: number; + pageIndex: number; + sortField: string; + sortDirection: string; + queryText?: string; +} + +export type AppPageState = { + [key in MlPages]?: Partial; +}; diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index 58eddba83db9d..512d12ca53253 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -89,3 +89,16 @@ export const mlCategory: Field = { type: ES_FIELD_TYPES.KEYWORD, aggregatable: false, }; + +export interface FieldAggCardinality { + field: string; + percent?: any; +} + +export interface ScriptAggCardinality { + script: any; +} + +export interface AggCardinality { + cardinality: FieldAggCardinality | ScriptAggCardinality; +} diff --git a/x-pack/plugins/ml/common/util/datafeed_utils.ts b/x-pack/plugins/ml/common/util/datafeed_utils.ts new file mode 100644 index 0000000000000..d86ee50baca19 --- /dev/null +++ b/x-pack/plugins/ml/common/util/datafeed_utils.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Aggregation, Datafeed } from '../types/anomaly_detection_jobs'; + +export const getDatafeedAggregations = ( + datafeedConfig: Partial | undefined +): Aggregation | undefined => { + if (datafeedConfig?.aggregations !== undefined) return datafeedConfig.aggregations; + if (datafeedConfig?.aggs !== undefined) return datafeedConfig.aggs; + return undefined; +}; + +export const getAggregationBucketsName = (aggregations: any): string | undefined => { + if (typeof aggregations === 'object') { + const keys = Object.keys(aggregations); + return keys.length > 0 ? keys[0] : undefined; + } +}; diff --git a/x-pack/plugins/ml/common/util/job_utils.test.ts b/x-pack/plugins/ml/common/util/job_utils.test.ts index a56ccd5208bab..1ea70c0c19b4e 100644 --- a/x-pack/plugins/ml/common/util/job_utils.test.ts +++ b/x-pack/plugins/ml/common/util/job_utils.test.ts @@ -188,8 +188,8 @@ describe('ML - job utils', () => { expect(isTimeSeriesViewDetector(job, 3)).toBe(false); }); - test('returns false for a detector using a script field as a metric field_name', () => { - expect(isTimeSeriesViewDetector(job, 4)).toBe(false); + test('returns true for a detector using a script field as a metric field_name', () => { + expect(isTimeSeriesViewDetector(job, 4)).toBe(true); }); }); @@ -281,6 +281,7 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 22)).toBe(true); expect(isSourceDataChartableForDetector(job, 23)).toBe(true); expect(isSourceDataChartableForDetector(job, 24)).toBe(true); + expect(isSourceDataChartableForDetector(job, 37)).toBe(true); }); test('returns false for expected detectors', () => { @@ -296,7 +297,6 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 34)).toBe(false); expect(isSourceDataChartableForDetector(job, 35)).toBe(false); expect(isSourceDataChartableForDetector(job, 36)).toBe(false); - expect(isSourceDataChartableForDetector(job, 37)).toBe(false); }); }); diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index a5b854a8d59a7..76990c61ff562 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -20,6 +20,7 @@ import { MlServerLimits } from '../types/ml_server_info'; import { JobValidationMessage, JobValidationMessageId } from '../constants/messages'; import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_types'; import { MLCATEGORY } from '../constants/field_types'; +import { getDatafeedAggregations } from './datafeed_utils'; export interface ValidationResults { valid: boolean; @@ -94,7 +95,6 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex // Perform extra check to see if the detector is using a scripted field. const scriptFields = Object.keys(job.datafeed_config.script_fields); isSourceDataChartable = - scriptFields.indexOf(dtr.field_name!) === -1 && scriptFields.indexOf(dtr.partition_field_name!) === -1 && scriptFields.indexOf(dtr.by_field_name!) === -1 && scriptFields.indexOf(dtr.over_field_name!) === -1; @@ -559,6 +559,27 @@ export function basicDatafeedValidation(datafeed: Datafeed): ValidationResults { }; } +export function basicJobAndDatafeedValidation(job: Job, datafeed: Datafeed): ValidationResults { + const messages: ValidationResults['messages'] = []; + let valid = true; + + if (datafeed && job) { + const datafeedAggregations = getDatafeedAggregations(datafeed); + + if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) { + valid = false; + messages.push({ id: 'missing_summary_count_field_name' }); + } + } + + return { + messages, + valid, + contains: (id) => messages.some((m) => id === m.id), + find: (id) => messages.find((m) => id === m.id), + }; +} + export function validateModelMemoryLimit(job: Job, limits: MlServerLimits): ValidationResults { const messages: ValidationResults['messages'] = []; let valid = true; diff --git a/x-pack/plugins/ml/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index 4691bac0a065a..ffb8b19dc9aa1 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -45,5 +45,5 @@ export function getGroupQueryText(groupIds: string[]): string { } export function getJobQueryText(jobIds: string | string[]): string { - return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; + return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : `id:${jobIds}`; } diff --git a/x-pack/plugins/ml/common/util/validation_utils.ts b/x-pack/plugins/ml/common/util/validation_utils.ts index ee4be34c6f600..b4f424a053b56 100644 --- a/x-pack/plugins/ml/common/util/validation_utils.ts +++ b/x-pack/plugins/ml/common/util/validation_utils.ts @@ -31,3 +31,22 @@ export function isValidJson(json: string) { return false; } } + +export function findAggField(aggs: Record, fieldName: string): any { + let value; + Object.keys(aggs).some(function (k) { + if (k === fieldName) { + value = aggs[k]; + return true; + } + if (aggs.hasOwnProperty(k) && typeof aggs[k] === 'object') { + value = findAggField(aggs[k], fieldName); + return value !== undefined; + } + }); + return value; +} + +export function isValidAggregationField(aggs: Record, fieldName: string): boolean { + return findAggField(aggs, fieldName) !== undefined; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx index 908755b197fd7..6bfd7a66331df 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx @@ -116,57 +116,59 @@ export const DecisionPathChart = ({ const tickFormatter = useCallback((d) => formatSingleValue(d, '').toString(), []); return ( - - - {baselineData && ( - - )} - - + + + {baselineData && ( + )} - showGridLines={false} - position={Position.Top} - showOverlappingTicks - domain={ - minDomain && maxDomain - ? { - min: minDomain, - max: maxDomain, - } - : undefined - } - /> - - - + /> + + + +
); }; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx index 496bc37f571ce..7b091a06d1064 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx @@ -98,6 +98,7 @@ export const ClassificationDecisionPath: FC = ( {options !== undefined && ( = ({ ]; return ( - <> +
{tabs.map((tab) => ( setSelectedTabId(tab.id)} key={tab.id} @@ -146,6 +147,6 @@ export const DecisionPathPopover: FC = ({ {selectedTabId === DECISION_PATH_TABS.JSON && ( )} - +
); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx index 64835e7ca4c6d..0fab1cf75259e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx @@ -210,6 +210,7 @@ export const FeatureImportanceSummaryPanel: FC - +
+ + - - - - + + + + +
) } /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 17ef84179ce63..63b7074ec3aaa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -7,10 +7,10 @@ import React, { FC, useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiInMemoryTable, EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, EuiSearchBar, EuiSearchBarProps, EuiSpacer, @@ -30,13 +30,12 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar'; import { CreateAnalyticsButton } from '../create_analytics_button'; -import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; import { filterAnalytics } from '../../../../common/search_bar_filters'; import { AnalyticsEmptyPrompt } from './empty_prompt'; import { useTableSettings } from './use_table_settings'; import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; -import { getGroupQueryText } from '../../../../../../../common/util/string_utils'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; const filters: EuiSearchBarProps['filters'] = [ { @@ -84,17 +83,28 @@ interface Props { isManagementTable?: boolean; isMlEnabledInSpace?: boolean; blockRefresh?: boolean; + pageState: ListingPageUrlState; + updatePageState: (update: Partial) => void; } export const DataFrameAnalyticsList: FC = ({ isManagementTable = false, isMlEnabledInSpace = true, blockRefresh = false, + pageState, + updatePageState, }) => { + const searchQueryText = pageState.queryText ?? ''; + const setSearchQueryText = useCallback( + (value) => { + updatePageState({ queryText: value }); + }, + [updatePageState] + ); + const [isInitialized, setIsInitialized] = useState(false); const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); const [filteredAnalytics, setFilteredAnalytics] = useState([]); - const [searchQueryText, setSearchQueryText] = useState(''); const [searchError, setSearchError] = useState(); const [analytics, setAnalytics] = useState([]); const [analyticsStats, setAnalyticsStats] = useState( @@ -102,9 +112,6 @@ export const DataFrameAnalyticsList: FC = ({ ); const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [errorMessage, setErrorMessage] = useState(undefined); - // Query text/job_id based on url but only after getAnalytics is done first - // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly - const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); const disabled = !checkPermission('canCreateDataFrameAnalytics') || @@ -119,17 +126,20 @@ export const DataFrameAnalyticsList: FC = ({ isManagementTable ); - const updateFilteredItems = (queryClauses: any) => { - if (queryClauses.length) { - const filtered = filterAnalytics(analytics, queryClauses); - setFilteredAnalytics(filtered); - } else { - setFilteredAnalytics(analytics); - } - }; + const updateFilteredItems = useCallback( + (queryClauses: any[]) => { + if (queryClauses.length) { + const filtered = filterAnalytics(analytics, queryClauses); + setFilteredAnalytics(filtered); + } else { + setFilteredAnalytics(analytics); + } + }, + [analytics] + ); const filterList = () => { - if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) { + if (searchQueryText !== '') { // trigger table filtering with query for job id to trigger table filter const query = EuiSearchBar.Query.parse(searchQueryText); let clauses: any = []; @@ -142,27 +152,9 @@ export const DataFrameAnalyticsList: FC = ({ } }; - useEffect(() => { - if (selectedIdFromUrlInitialized === false && analytics.length > 0) { - const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); - let queryText = ''; - - if (groupIds !== undefined) { - queryText = getGroupQueryText(groupIds); - } else if (jobId !== undefined) { - queryText = jobId; - } - - setSelectedIdFromUrlInitialized(true); - setSearchQueryText(queryText); - } else { - filterList(); - } - }, [selectedIdFromUrlInitialized, analytics]); - useEffect(() => { filterList(); - }, [selectedIdFromUrlInitialized, searchQueryText]); + }, [searchQueryText]); const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); @@ -183,19 +175,19 @@ export const DataFrameAnalyticsList: FC = ({ ); const { onTableChange, pagination, sorting } = useTableSettings( - DataFrameAnalyticsListColumn.id, - filteredAnalytics + filteredAnalytics, + pageState, + updatePageState ); const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => { if (search.error !== null) { setSearchError(search.error.message); - return false; + return; } setSearchError(undefined); setSearchQueryText(search.queryText); - return true; }; // Before the analytics have been loaded for the first time, display the loading indicator only. @@ -251,6 +243,7 @@ export const DataFrameAnalyticsList: FC = ({ ); + const search: EuiSearchBarProps = { query: searchQueryText, onChange: handleSearchOnChange, @@ -284,15 +277,13 @@ export const DataFrameAnalyticsList: FC = ({
allowNeutralSort={false} - className="mlAnalyticsInMemoryTable" columns={columns} - error={searchError} hasActions={false} isExpandable={true} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} isSelectable={false} items={analytics} itemId={DataFrameAnalyticsListColumn.id} - itemIdToExpandedRowMap={itemIdToExpandedRowMap} loading={isLoading} onTableChange={onTableChange} pagination={pagination} @@ -302,6 +293,7 @@ export const DataFrameAnalyticsList: FC = ({ rowProps={(item) => ({ 'data-test-subj': `mlAnalyticsTableRow row-${item.id}`, })} + error={searchError} />
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 8c7c8b9db8b64..84c37ac8b816b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -116,14 +116,14 @@ export interface DataFrameAnalyticsListRow { } // Used to pass on attribute names to table columns -export enum DataFrameAnalyticsListColumn { - configDestIndex = 'config.dest.index', - configSourceIndex = 'config.source.index', - configCreateTime = 'config.create_time', - description = 'config.description', - id = 'id', - memoryStatus = 'stats.memory_usage.status', -} +export const DataFrameAnalyticsListColumn = { + configDestIndex: 'config.dest.index', + configSourceIndex: 'config.source.index', + configCreateTime: 'config.create_time', + description: 'config.description', + id: 'id', + memoryStatus: 'stats.memory_usage.status', +} as const; export type ItemIdToExpandedRowMap = Record; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 2b63b9e780819..93868ce0c17e6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -135,13 +135,13 @@ export const progressColumn = { 'data-test-subj': 'mlAnalyticsTableColumnProgress', }; -export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => { +export const DFAnalyticsJobIdLink = ({ jobId }: { jobId: string }) => { const href = useMlLink({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, - pageState: { jobId: item.id }, + pageState: { jobId }, }); - return {item.id}; + return {jobId}; }; export const useColumns = ( @@ -199,13 +199,17 @@ export const useColumns = ( 'data-test-subj': 'mlAnalyticsTableRowDetailsToggle', }, { - name: 'ID', + field: DataFrameAnalyticsListColumn.id, + name: i18n.translate('xpack.ml.dataframe.analyticsList.id', { + defaultMessage: 'ID', + }), sortable: (item: DataFrameAnalyticsListRow) => item.id, truncateText: true, 'data-test-subj': 'mlAnalyticsTableColumnId', scope: 'row', - render: (item: DataFrameAnalyticsListRow) => - isManagementTable ? : item.id, + render: (jobId: string) => { + return isManagementTable ? : jobId; + }, }, { field: DataFrameAnalyticsListColumn.description, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index 5b7d71dacccf8..68774fb86fe96 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState } from 'react'; import { Direction, EuiBasicTableProps, Pagination, PropertySort } from '@elastic/eui'; +import { useCallback, useMemo } from 'react'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; -const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; // Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange @@ -29,15 +29,6 @@ export interface CriteriaWithPagination extends Criteria { }; } -interface AnalyticsBasicTableSettings { - pageIndex: number; - pageSize: number; - totalItemCount: number; - hidePerPageOptions: boolean; - sortField: keyof T; - sortDirection: Direction; -} - interface UseTableSettingsReturnValue { onTableChange: EuiBasicTableProps['onChange']; pagination: Pagination; @@ -45,49 +36,44 @@ interface UseTableSettingsReturnValue { } export function useTableSettings( - sortByField: keyof TypeOfItem, - items: TypeOfItem[] + items: TypeOfItem[], + pageState: ListingPageUrlState, + updatePageState: (update: Partial) => void ): UseTableSettingsReturnValue { - const [tableSettings, setTableSettings] = useState>({ - pageIndex: 0, - pageSize: PAGE_SIZE, - totalItemCount: 0, - hidePerPageOptions: false, - sortField: sortByField, - sortDirection: 'asc', - }); - - const onTableChange: EuiBasicTableProps['onChange'] = ({ - page = { index: 0, size: PAGE_SIZE }, - sort = { field: sortByField, direction: 'asc' }, - }: CriteriaWithPagination) => { - const { index, size } = page; - const { field, direction } = sort; - - setTableSettings({ - ...tableSettings, - pageIndex: index, - pageSize: size, - sortField: field, - sortDirection: direction, - }); - }; + const { pageIndex, pageSize, sortField, sortDirection } = pageState; - const { pageIndex, pageSize, sortField, sortDirection } = tableSettings; + const onTableChange: EuiBasicTableProps['onChange'] = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + const result = { + pageIndex: page?.index ?? pageState.pageIndex, + pageSize: page?.size ?? pageState.pageSize, + sortField: (sort?.field as string) ?? pageState.sortField, + sortDirection: sort?.direction ?? pageState.sortDirection, + }; + updatePageState(result); + }, + [pageState, updatePageState] + ); - const pagination = { - pageIndex, - pageSize, - totalItemCount: items.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - }; + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount: items.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }), + [items, pageIndex, pageSize] + ); - const sorting = { - sort: { - field: sortField as string, - direction: sortDirection, - }, - }; + const sorting = useMemo( + () => ({ + sort: { + field: sortField as string, + direction: sortDirection as Direction, + }, + }), + [sortField, sortDirection] + ); return { onTableChange, pagination, sorting }; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index eaeae6cc64520..a5d3555fcc278 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -50,9 +50,12 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string return navTabs; }, [jobId !== undefined]); - const onTabClick = useCallback(async (tab: Tab) => { - await navigateToPath(tab.path, true); - }, []); + const onTabClick = useCallback( + async (tab: Tab) => { + await navigateToPath(tab.path, true); + }, + [navigateToPath] + ); return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts index 7c70a25071640..77c794dce10ce 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts @@ -6,8 +6,8 @@ export * from './models_list'; -export enum ModelsTableToConfigMapping { - id = 'model_id', - createdAt = 'create_time', - type = 'type', -} +export const ModelsTableToConfigMapping = { + id: 'model_id', + createdAt: 'create_time', + type: 'type', +} as const; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index a87f11df937d3..2d74d08c4550c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -52,6 +52,8 @@ import { filterAnalyticsModels } from '../../../../common/search_bar_filters'; import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; import { timeFormatter } from '../../../../../../../common/util/date_utils'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; +import { usePageUrlState } from '../../../../../util/url_state'; type Stats = Omit; @@ -63,6 +65,13 @@ export type ModelItem = TrainedModelConfigResponse & { export type ModelItemFull = Required; +export const getDefaultModelsListState = (): ListingPageUrlState => ({ + pageIndex: 0, + pageSize: 10, + sortField: ModelsTableToConfigMapping.id, + sortDirection: 'asc', +}); + export const ModelsList: FC = () => { const { services: { @@ -71,12 +80,24 @@ export const ModelsList: FC = () => { } = useMlKibana(); const urlGenerator = useMlUrlGenerator(); + const [pageState, updatePageState] = usePageUrlState( + ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE, + getDefaultModelsListState() + ); + + const searchQueryText = pageState.queryText ?? ''; + const setSearchQueryText = useCallback( + (value) => { + updatePageState({ queryText: value }); + }, + [updatePageState] + ); + const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean; const trainedModelsApiService = useTrainedModelsApiService(); const { toasts } = useNotifications(); - const [searchQueryText, setSearchQueryText] = useState(''); const [filteredModels, setFilteredModels] = useState([]); const [isLoading, setIsLoading] = useState(false); const [items, setItems] = useState([]); @@ -432,8 +453,9 @@ export const ModelsList: FC = () => { : []; const { onTableChange, pagination, sorting } = useTableSettings( - ModelsTableToConfigMapping.id, - filteredModels + filteredModels, + pageState, + updatePageState ); const toolsLeft = ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 44085384f7536..5a17b91818a1c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -33,11 +33,27 @@ import { UpgradeWarning } from '../../../components/upgrade'; import { AnalyticsNavigationBar } from './components/analytics_navigation_bar'; import { ModelsList } from './components/models_management'; import { JobMap } from '../job_map'; +import { usePageUrlState } from '../../../util/url_state'; +import { ListingPageUrlState } from '../../../../../common/types/common'; +import { DataFrameAnalyticsListColumn } from './components/analytics_list/common'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; + +export const getDefaultDFAListState = (): ListingPageUrlState => ({ + pageIndex: 0, + pageSize: 10, + sortField: DataFrameAnalyticsListColumn.id, + sortDirection: 'asc', +}); export const Page: FC = () => { const [blockRefresh, setBlockRefresh] = useState(false); const [globalState] = useUrlState('_g'); + const [dfaPageState, setDfaPageState] = usePageUrlState( + ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + getDefaultDFAListState() + ); + useRefreshInterval(setBlockRefresh); const location = useLocation(); @@ -59,16 +75,13 @@ export const Page: FC = () => { />   @@ -96,7 +109,11 @@ export const Page: FC = () => { {selectedTabId === 'map' && mapJobId && } {selectedTabId === 'data_frame_analytics' && ( - + )} {selectedTabId === 'models' && } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 39166841a4e1b..95c721a7043dc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -123,7 +123,8 @@ export const anomalyDataChange = function ( config.timeField, range.min, range.max, - bucketSpanSeconds * 1000 + bucketSpanSeconds * 1000, + config.datafeedConfig ) .toPromise(); } else { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx index f0fa62b7a3d8a..1b1bea889925f 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -22,8 +22,6 @@ import { JobGroup } from '../job_group'; import { useMlKibana } from '../../../../contexts/kibana'; interface JobFilterBarProps { - jobId: string; - groupIds: string[]; setFilters: (query: Query | null) => void; queryText?: string; } @@ -75,7 +73,7 @@ export const JobFilterBar: FC = ({ queryText, setFilters }) = useEffect(() => { setFilters(queryInstance); - }, []); + }, [queryText]); const filters: SearchFilterConfig[] = useMemo( () => [ diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 397062248689d..338222e3ac4a2 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -6,7 +6,6 @@ import { each } from 'lodash'; import { i18n } from '@kbn/i18n'; -import rison from 'rison-node'; import { mlJobService } from '../../../services/job_service'; import { @@ -367,31 +366,3 @@ function jobProperty(job, prop) { }; return job[propMap[prop]]; } - -function getUrlVars(url) { - const vars = {}; - url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (_, key, value) { - vars[key] = value; - }); - return vars; -} - -export function getSelectedIdFromUrl(url) { - const result = {}; - if (typeof url === 'string') { - const isGroup = url.includes('groupIds'); - url = decodeURIComponent(url); - - if (url.includes('mlManagement')) { - const urlParams = getUrlVars(url); - const decodedJson = rison.decode(urlParams.mlManagement); - - if (isGroup) { - result.groupIds = decodedJson.groupIds; - } else { - result.jobId = decodedJson.jobId; - } - } - } - return result; -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts deleted file mode 100644 index 4414be0b4fdcb..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getSelectedIdFromUrl } from './utils'; - -describe('ML - Jobs List utils', () => { - const jobId = 'test_job_id_1'; - const jobIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(jobId:${jobId})`; - const groupIdOne = 'test_group_id_1'; - const groupIdTwo = 'test_group_id_2'; - const groupIdsUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne},${groupIdTwo}))`; - const groupIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne}))`; - - describe('getSelectedIdFromUrl', () => { - it('should get selected job id from the url', () => { - const actual = getSelectedIdFromUrl(jobIdUrl); - expect(actual).toStrictEqual({ jobId }); - }); - - it('should get selected group ids from the url', () => { - const expected = { groupIds: [groupIdOne, groupIdTwo] }; - const actual = getSelectedIdFromUrl(groupIdsUrl); - expect(actual).toStrictEqual(expected); - }); - - it('should get selected group id from the url', () => { - const expected = { groupIds: [groupIdOne] }; - const actual = getSelectedIdFromUrl(groupIdUrl); - expect(actual).toStrictEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index 4c6469f6800a7..df50f53b811fa 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; // @ts-ignore import { JobsListView } from './components/jobs_list_view/index'; -import { useUrlState } from '../../util/url_state'; +import { usePageUrlState } from '../../util/url_state'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { ListingPageUrlState } from '../../../../common/types/common'; interface JobsPageProps { blockRefresh?: boolean; @@ -17,15 +19,7 @@ interface JobsPageProps { lastRefresh?: number; } -export interface AnomalyDetectionJobsListState { - pageSize: number; - pageIndex: number; - sortField: string; - sortDirection: string; - queryText?: string; -} - -export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsListState => ({ +export const getDefaultAnomalyDetectionJobsListState = (): ListingPageUrlState => ({ pageIndex: 0, pageSize: 10, sortField: 'id', @@ -33,33 +27,15 @@ export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsL }); export const JobsPage: FC = (props) => { - const [appState, setAppState] = useUrlState('_a'); - - const jobListState: AnomalyDetectionJobsListState = useMemo(() => { - return { - ...getDefaultAnomalyDetectionJobsListState(), - ...(appState ?? {}), - }; - }, [appState]); - - const onJobsViewStateUpdate = useCallback( - (update: Partial) => { - setAppState({ - ...jobListState, - ...update, - }); - }, - [appState, setAppState] + const [pageState, setPageState] = usePageUrlState( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + getDefaultAnomalyDetectionJobsListState() ); return (
- +
); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts index 6671aaa83abe0..f23807f156576 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts @@ -134,6 +134,7 @@ export const useModelMemoryEstimator = ( // Update model memory estimation payload on the job creator updates useEffect(() => { modelMemoryEstimator.update({ + datafeedConfig: jobCreator.datafeedConfig, analysisConfig: jobCreator.jobConfig.analysis_config, indexPattern: jobCreator.indexPatternTitle, query: jobCreator.datafeedConfig.query, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 635322a6c4469..1c012033e97c8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -10,6 +10,7 @@ import { map, startWith, tap } from 'rxjs/operators'; import { basicJobValidation, basicDatafeedValidation, + basicJobAndDatafeedValidation, } from '../../../../../../common/util/job_utils'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_creator'; @@ -53,6 +54,7 @@ export interface BasicValidations { scrollSize: Validation; categorizerMissingPerPartition: Validation; categorizerVaryingPerPartitionField: Validation; + summaryCountField: Validation; } export interface AdvancedValidations { @@ -80,6 +82,7 @@ export class JobValidator { scrollSize: { valid: true }, categorizerMissingPerPartition: { valid: true }, categorizerVaryingPerPartitionField: { valid: true }, + summaryCountField: { valid: true }, }; private _advancedValidations: AdvancedValidations = { categorizationFieldValid: { valid: true }, @@ -197,6 +200,14 @@ export class JobValidator { datafeedConfig ); + const basicJobAndDatafeedResults = basicJobAndDatafeedValidation(jobConfig, datafeedConfig); + populateValidationMessages( + basicJobAndDatafeedResults, + this._basicValidations, + jobConfig, + datafeedConfig + ); + // run addition job and group id validation const idResults = checkForExistingJobAndGroupIds( this._jobCreator.jobId, @@ -228,6 +239,9 @@ export class JobValidator { public get bucketSpan(): Validation { return this._basicValidations.bucketSpan; } + public get summaryCountField(): Validation { + return this._basicValidations.summaryCountField; + } public get duplicateDetectors(): Validation { return this._basicValidations.duplicateDetectors; @@ -297,6 +311,7 @@ export class JobValidator { this.duplicateDetectors.valid && this.categorizerMissingPerPartition.valid && this.categorizerVaryingPerPartitionField.valid && + this.summaryCountField.valid && !this.validating && (this._jobCreator.type !== JOB_TYPE.CATEGORIZATION || (this._jobCreator.type === JOB_TYPE.CATEGORIZATION && this.categorizationField)) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index 1ce81bf0dcdf0..04be935ed4399 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -193,6 +193,15 @@ export function populateValidationMessages( basicValidations.frequency.valid = false; basicValidations.frequency.message = invalidTimeIntervalMessage(datafeedConfig.frequency); } + if (validationResults.contains('missing_summary_count_field_name')) { + basicValidations.summaryCountField.valid = false; + basicValidations.summaryCountField.message = i18n.translate( + 'xpack.ml.newJob.wizard.validateJob.summaryCountFieldMissing', + { + defaultMessage: 'Required field as the datafeed uses aggregations.', + } + ); + } } export function checkForExistingJobAndGroupIds( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx index 0dd802855ea67..cf98625672019 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -61,9 +61,12 @@ export const DatafeedPreview: FC<{ if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { try { const resp = await mlJobService.searchPreview(combinedJob); - const data = resp.aggregations - ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) - : resp.hits.hits; + let data = resp.hits.hits; + // the first item under aggregations can be any name + if (typeof resp.aggregations === 'object' && Object.keys(resp.aggregations).length > 0) { + const accessor = Object.keys(resp.aggregations)[0]; + data = resp.aggregations[accessor].buckets.slice(0, ML_DATA_PREVIEW_COUNT); + } setPreviewJsonString(JSON.stringify(data, null, 2)); } catch (error) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx index 5109718268ac3..a09b6540e101f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx @@ -7,23 +7,44 @@ import React, { memo, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; +import { Validation } from '../../../../../common/job_validator'; +import { useMlKibana } from '../../../../../../../contexts/kibana'; -export const Description: FC = memo(({ children }) => { +interface Props { + validation: Validation; +} + +export const Description: FC = memo(({ children, validation }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title', { defaultMessage: 'Summary count field', }); + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-aggregation.html`; return ( {title}} description={ + + + ), + }} /> } > - + <>{children} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx index af759117b8501..70eaa39f71c69 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx @@ -17,13 +17,23 @@ import { import { Description } from './description'; export const SummaryCountField: FC = () => { - const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + jobValidator, + jobValidatorUpdated, + } = useContext(JobCreatorContext); const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator | AdvancedJobCreator; const { fields } = newJobCapsService; const [summaryCountFieldName, setSummaryCountFieldName] = useState( jobCreator.summaryCountFieldName ); + const [validation, setValidation] = useState(jobValidator.summaryCountField); + useEffect(() => { + setValidation(jobValidator.summaryCountField); + }, [jobValidatorUpdated]); useEffect(() => { jobCreator.summaryCountFieldName = summaryCountFieldName; @@ -35,7 +45,7 @@ export const SummaryCountField: FC = () => { }, [jobCreatorUpdated]); return ( - + ( - getDefaultAnomalyDetectionJobsListState() - ); +function usePageState( + defaultState: T +): [T, (update: Partial) => void] { + const [pageState, setPageState] = useState(defaultState); const updateState = useCallback( - (update: Partial) => { - setJobsViewState({ - ...jobsViewState, + (update: Partial) => { + setPageState({ + ...pageState, ...update, }); }, - [jobsViewState] + [pageState] ); + return [pageState, updateState]; +} + +function useTabs(isMlEnabledInSpace: boolean): Tab[] { + const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState()); + const [dfaPageState, updateDfaPageState] = usePageState(getDefaultDFAListState()); + return useMemo( () => [ { @@ -75,8 +81,8 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { @@ -95,12 +101,14 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { ), }, ], - [isMlEnabledInSpace, jobsViewState, updateState] + [isMlEnabledInSpace, adPageState, updateAdPageState, dfaPageState, updateDfaPageState] ); } diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 80e93b2f1f5d9..b67b5015dbd6c 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -628,6 +628,7 @@ export function mlApiServicesProvider(httpService: HttpService) { }, calculateModelMemoryLimit$({ + datafeedConfig, analysisConfig, indexPattern, query, @@ -635,6 +636,7 @@ export function mlApiServicesProvider(httpService: HttpService) { earliestMs, latestMs, }: { + datafeedConfig?: Datafeed; analysisConfig: AnalysisConfig; indexPattern: string; query: any; @@ -643,6 +645,7 @@ export function mlApiServicesProvider(httpService: HttpService) { latestMs: number; }) { const body = JSON.stringify({ + datafeedConfig, analysisConfig, indexPattern, query, diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 2869a7439614f..79afe2ba5a0ad 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -16,9 +16,11 @@ import { map } from 'rxjs/operators'; import { each, get } from 'lodash'; import { Dictionary } from '../../../../common/types/common'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; -import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { Datafeed, JobId } from '../../../../common/types/anomaly_detection_jobs'; import { MlApiServices } from '../ml_api_service'; import { CriteriaField } from './index'; +import { findAggField } from '../../../../common/util/validation_utils'; +import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils'; import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; interface ResultResponse { @@ -69,8 +71,12 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { timeFieldName: string, earliestMs: number, latestMs: number, - intervalMs: number + intervalMs: number, + datafeedConfig?: Datafeed ): Observable { + const scriptFields = datafeedConfig?.script_fields; + const aggFields = getDatafeedAggregations(datafeedConfig); + // Build the criteria to use in the bool filter part of the request. // Add criteria for the time range, entity fields, // plus any additional supplied query. @@ -151,15 +157,35 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { body.aggs.byTime.aggs = {}; const metricAgg: any = { - [metricFunction]: { - field: metricFieldName, - }, + [metricFunction]: {}, }; + if (scriptFields !== undefined && scriptFields[metricFieldName] !== undefined) { + metricAgg[metricFunction].script = scriptFields[metricFieldName].script; + } else { + metricAgg[metricFunction].field = metricFieldName; + } if (metricFunction === 'percentiles') { metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; } - body.aggs.byTime.aggs.metric = metricAgg; + + // when the field is an aggregation field, because the field doesn't actually exist in the indices + // we need to pass all the sub aggs from the original datafeed config + // so that we can access the aggregated field + if (typeof aggFields === 'object' && Object.keys(aggFields).length > 0) { + // first item under aggregations can be any name, not necessarily 'buckets' + const accessor = Object.keys(aggFields)[0]; + const tempAggs = { ...(aggFields[accessor].aggs ?? aggFields[accessor].aggregations) }; + const foundValue = findAggField(tempAggs, metricFieldName); + + if (foundValue !== undefined) { + tempAggs.metric = foundValue; + delete tempAggs[metricFieldName]; + } + body.aggs.byTime.aggs = tempAggs; + } else { + body.aggs.byTime.aggs.metric = metricAgg; + } } return mlApiServices.esSearch$({ index, body }).pipe( diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index d053d69b4d1f2..8419660a52a9a 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -286,7 +286,7 @@ export function resultsServiceProvider(mlApiServices) { influencerFieldValues: { terms: { field: 'influencer_field_value', - size: maxFieldValues, + size: !!maxFieldValues ? maxFieldValues : ANOMALY_SWIM_LANE_HARD_LIMIT, order: { maxAnomalyScore: 'desc', }, @@ -416,7 +416,7 @@ export function resultsServiceProvider(mlApiServices) { influencerFieldValues: { terms: { field: 'influencer_field_value', - size: maxResults !== undefined ? maxResults : 2, + size: !!maxResults ? maxResults : 2, order: { maxAnomalyScore: 'desc', }, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 8df186c5c3c6e..b2d054becbb1a 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -51,6 +51,7 @@ import { unhighlightFocusChartAnnotation, ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; +import { distinctUntilChanged } from 'rxjs/operators'; const focusZoomPanelHeight = 25; const focusChartHeight = 310; @@ -570,6 +571,7 @@ class TimeseriesChartIntl extends Component { } renderFocusChart() { + console.log('renderFocusChart'); const { focusAggregationInterval, focusAnnotationData: focusAnnotationDataOriginalPropValue, @@ -1798,7 +1800,15 @@ class TimeseriesChartIntl extends Component { } export const TimeseriesChart = (props) => { - const annotationProp = useObservable(annotation$); + const annotationProp = useObservable( + annotation$.pipe( + distinctUntilChanged((prev, curr) => { + // prevent re-rendering + return prev !== null && curr !== null; + }) + ) + ); + if (annotationProp === undefined) { return null; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 0d7abdab90be0..90c39497a9a18 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -94,7 +94,8 @@ function getMetricData( chartConfig.timeField, earliestMs, latestMs, - intervalMs + intervalMs, + chartConfig?.datafeedConfig ) .pipe( map((resp) => { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index e3b6e38f47bab..f14f11e5d6149 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1014,6 +1014,7 @@ export class TimeSeriesExplorer extends React.Component { this.previousShowForecast = showForecast; this.previousShowModelBounds = showModelBounds; + console.log('Timeseriesexplorer rerendered'); return ( {fieldNamesWithEmptyValues.length > 0 && ( diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index a3c70e1130904..448a888ab32c2 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -13,6 +13,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import { Dictionary } from '../../../common/types/common'; import { getNestedProperty } from './object_utils'; +import { MlPages } from '../../../common/constants/ml_url_generator'; type Accessor = '_a' | '_g'; export type SetUrlState = ( @@ -150,3 +151,35 @@ export const useUrlState = (accessor: Accessor) => { ); return [urlState, setUrlState]; }; + +/** + * Hook for managing the URL state of the page. + */ +export const usePageUrlState = ( + pageKey: MlPages, + defaultState: PageUrlState +): [PageUrlState, (update: Partial) => void] => { + const [appState, setAppState] = useUrlState('_a'); + const pageState = appState?.[pageKey]; + + const resultPageState: PageUrlState = useMemo(() => { + return { + ...defaultState, + ...(pageState ?? {}), + }; + }, [pageState]); + + const onStateUpdate = useCallback( + (update: Partial, replace?: boolean) => { + setAppState(pageKey, { + ...(replace ? {} : resultPageState), + ...update, + }); + }, + [pageKey, resultPageState, setAppState] + ); + + return useMemo(() => { + return [resultPageState, onStateUpdate]; + }, [resultPageState, onStateUpdate]); +}; diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index 717d293ccd7fa..6d7e286a29476 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -19,8 +19,8 @@ import type { import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { createGenericMlUrl } from './common'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; -import type { AnomalyDetectionJobsListState } from '../application/jobs/jobs_list/jobs'; import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils'; +import { AppPageState, ListingPageUrlState } from '../../common/types/common'; /** * Creates URL to the Anomaly Detection Job management page */ @@ -41,11 +41,15 @@ export function createAnomalyDetectionJobManagementUrl( if (groupIds) { queryTextArr.push(getGroupQueryText(groupIds)); } - const queryState: Partial = { + const jobsListState: Partial = { ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), }; - url = setStateToKbnUrl>( + const queryState: AppPageState = { + [ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE]: jobsListState, + }; + + url = setStateToKbnUrl>( '_a', queryState, { useHash: false, storeInHashQuery: false }, diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 6c58a9d28bcc2..dc9c3bd86cc63 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -10,12 +10,13 @@ import { DataFrameAnalyticsExplorationQueryState, DataFrameAnalyticsExplorationUrlState, - DataFrameAnalyticsQueryState, DataFrameAnalyticsUrlState, MlCommonGlobalState, } from '../../common/types/ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; +import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils'; +import { AppPageState, ListingPageUrlState } from '../../common/types/common'; export function createDataFrameAnalyticsJobManagementUrl( appBasePath: string, @@ -26,13 +27,23 @@ export function createDataFrameAnalyticsJobManagementUrl( if (mlUrlGeneratorState) { const { jobId, groupIds, globalState } = mlUrlGeneratorState; if (jobId || groupIds) { - const queryState: Partial = { - jobId, - groupIds, + const queryTextArr = []; + if (jobId) { + queryTextArr.push(getJobQueryText(jobId)); + } + if (groupIds) { + queryTextArr.push(getGroupQueryText(groupIds)); + } + const jobsListState: Partial = { + ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), + }; + + const queryState: AppPageState = { + [ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE]: jobsListState, }; - url = setStateToKbnUrl>( - 'mlManagement', + url = setStateToKbnUrl>( + '_a', queryState, { useHash: false, storeInHashQuery: false }, url diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index e7f12ead3ffe9..3f3d88f1a31d9 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -30,7 +30,7 @@ describe('MlUrlGenerator', () => { jobId: 'fq_single_1', }, }); - expect(url).toBe('/app/ml/jobs?_a=(queryText:fq_single_1)'); + expect(url).toBe("/app/ml/jobs?_a=(jobs:(queryText:'id:fq_single_1'))"); }); it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { @@ -40,7 +40,9 @@ describe('MlUrlGenerator', () => { groupIds: ['farequote', 'categorization'], }, }); - expect(url).toBe("/app/ml/jobs?_a=(queryText:'groups:(farequote%20or%20categorization)')"); + expect(url).toBe( + "/app/ml/jobs?_a=(jobs:(queryText:'groups:(farequote%20or%20categorization)'))" + ); }); it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { @@ -180,7 +182,9 @@ describe('MlUrlGenerator', () => { jobId: 'grid_regression_1', }, }); - expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(jobId:grid_regression_1)'); + expect(url).toBe( + "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'id:grid_regression_1'))" + ); }); it('should generate valid URL for the Data Frame Analytics job management page with groupIds', async () => { @@ -190,7 +194,9 @@ describe('MlUrlGenerator', () => { groupIds: ['group_1', 'group_2'], }, }); - expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(groupIds:!(group_1,group_2))'); + expect(url).toBe( + "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'groups:(group_1%20or%20group_2)'))" + ); }); }); diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts index 180b4e71dfa9c..865f305f2ff9f 100644 --- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts @@ -7,7 +7,7 @@ import numeral from '@elastic/numeral'; import { IScopedClusterClient } from 'kibana/server'; import { MLCATEGORY } from '../../../common/constants/field_types'; -import { AnalysisConfig } from '../../../common/types/anomaly_detection_jobs'; +import { AnalysisConfig, Datafeed } from '../../../common/types/anomaly_detection_jobs'; import { fieldsServiceProvider } from '../fields_service'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import type { MlClient } from '../../lib/ml_client'; @@ -46,7 +46,8 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => { query: any, timeFieldName: string, earliestMs: number, - latestMs: number + latestMs: number, + datafeedConfig?: Datafeed ): Promise<{ overallCardinality: { [key: string]: number }; maxBucketCardinality: { [key: string]: number }; @@ -101,7 +102,8 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => { query, timeFieldName, earliestMs, - latestMs + latestMs, + datafeedConfig ); } @@ -142,7 +144,8 @@ export function calculateModelMemoryLimitProvider( timeFieldName: string, earliestMs: number, latestMs: number, - allowMMLGreaterThanMax = false + allowMMLGreaterThanMax = false, + datafeedConfig?: Datafeed ): Promise { const { body: info } = await mlClient.info(); const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); @@ -154,7 +157,8 @@ export function calculateModelMemoryLimitProvider( query, timeFieldName, earliestMs, - latestMs + latestMs, + datafeedConfig ); const { body } = await mlClient.estimateModelMemory({ diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 1f59e990096a4..0142e44276eee 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -15,6 +15,9 @@ import { buildSamplerAggregation, getSamplerAggregationsResponsePath, } from '../../lib/query_utils'; +import { AggCardinality } from '../../../common/types/fields'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; +import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; const SAMPLER_TOP_TERMS_THRESHOLD = 100000; const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; @@ -121,12 +124,6 @@ interface AggHistogram { }; } -interface AggCardinality { - cardinality: { - field: string; - }; -} - interface AggTerms { terms: { field: string; @@ -597,23 +594,35 @@ export class DataVisualizer { samplerShardSize: number, timeFieldName: string, earliestMs?: number, - latestMs?: number + latestMs?: number, + datafeedConfig?: Datafeed ) { const index = indexPatternTitle; const size = 0; const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); // Value count aggregation faster way of checking if field exists than using // filter aggregation with exists query. - const aggs: Aggs = {}; + const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {}; + aggregatableFields.forEach((field, i) => { const safeFieldName = getSafeAggregationName(field, i); aggs[`${safeFieldName}_count`] = { filter: { exists: { field } }, }; - aggs[`${safeFieldName}_cardinality`] = { - cardinality: { field }, - }; + + let cardinalityField: AggCardinality; + if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { + cardinalityField = aggs[`${safeFieldName}_cardinality`] = { + cardinality: { script: datafeedConfig?.script_fields[field].script }, + }; + } else { + cardinalityField = { + cardinality: { field }, + }; + } + aggs[`${safeFieldName}_cardinality`] = cardinalityField; }); const searchBody = { @@ -661,10 +670,27 @@ export class DataVisualizer { }, }); } else { - stats.aggregatableNotExistsFields.push({ - fieldName: field, - existsInDocs: false, - }); + if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { + const cardinality = get( + aggregations, + [...aggsPath, `${safeFieldName}_cardinality`, 'value'], + 0 + ); + stats.aggregatableExistsFields.push({ + fieldName: field, + existsInDocs: true, + stats: { + sampleCount, + count, + cardinality, + }, + }); + } else { + stats.aggregatableNotExistsFields.push({ + fieldName: field, + existsInDocs: false, + }); + } } }); diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index ed8d3f48e387c..17f35967a626d 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -9,6 +9,10 @@ import { IScopedClusterClient } from 'kibana/server'; import { duration } from 'moment'; import { parseInterval } from '../../../common/util/parse_interval'; import { initCardinalityFieldsCache } from './fields_aggs_cache'; +import { AggCardinality } from '../../../common/types/fields'; +import { isValidAggregationField } from '../../../common/util/validation_utils'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; +import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; /** * Service for carrying out queries to obtain data @@ -35,14 +39,29 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { */ async function getAggregatableFields( index: string | string[], - fieldNames: string[] + fieldNames: string[], + datafeedConfig?: Datafeed ): Promise { const { body } = await asCurrentUser.fieldCaps({ index, fields: fieldNames, }); const aggregatableFields: string[] = []; + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); + fieldNames.forEach((fieldName) => { + if ( + typeof datafeedConfig?.script_fields === 'object' && + datafeedConfig.script_fields.hasOwnProperty(fieldName) + ) { + aggregatableFields.push(fieldName); + } + if ( + datafeedAggregations !== undefined && + isValidAggregationField(datafeedAggregations, fieldName) + ) { + aggregatableFields.push(fieldName); + } const fieldInfo = body.fields[fieldName]; const typeKeys = fieldInfo !== undefined ? Object.keys(fieldInfo) : []; if (typeKeys.length > 0) { @@ -67,10 +86,12 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { query: any, timeFieldName: string, earliestMs: number, - latestMs: number + latestMs: number, + datafeedConfig?: Datafeed ): Promise<{ [key: string]: number }> { - const aggregatableFields = await getAggregatableFields(index, fieldNames); + const aggregatableFields = await getAggregatableFields(index, fieldNames, datafeedConfig); + // getAggregatableFields doesn't account for scripted or aggregated fields if (aggregatableFields.length === 0) { return {}; } @@ -112,10 +133,22 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { mustCriteria.push(query); } - const aggs = fieldsToAgg.reduce((obj, field) => { - obj[field] = { cardinality: { field } }; - return obj; - }, {} as { [field: string]: { cardinality: { field: string } } }); + const aggs = fieldsToAgg.reduce( + (obj, field) => { + if ( + typeof datafeedConfig?.script_fields === 'object' && + datafeedConfig.script_fields.hasOwnProperty(field) + ) { + obj[field] = { cardinality: { script: datafeedConfig.script_fields[field].script } }; + } else { + obj[field] = { cardinality: { field } }; + } + return obj; + }, + {} as { + [field: string]: AggCardinality; + } + ); const body = { query: { diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index e3fcc69596dc9..3526f9cebb89b 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -27,6 +27,7 @@ import { validateTimeRange, isValidTimeField } from './validate_time_range'; import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlClient } from '../../lib/ml_client'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; export type ValidateJobPayload = TypeOf; @@ -100,6 +101,12 @@ export async function validateJob( ...(await validateModelMemoryLimit(client, mlClient, job, duration)) ); } + + // if datafeed has aggregation, require job config to include a valid summary_doc_field_name + const datafeedAggregations = getDatafeedAggregations(job.datafeed_config); + if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) { + validationMessages.push({ id: 'missing_summary_count_field_name' }); + } } else { validationMessages = basicValidation.messages; validationMessages.push({ id: 'skipped_extended_tests' }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts index c5822b863c83d..f2bcc6e50d86e 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -11,6 +11,8 @@ import { validateJobObject } from './validate_job_object'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { Detector } from '../../../common/types/anomaly_detection_jobs'; import { MessageId, JobValidationMessage } from '../../../common/constants/messages'; +import { isValidAggregationField } from '../../../common/util/validation_utils'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; function isValidCategorizationConfig(job: CombinedJob, fieldName: string): boolean { return ( @@ -66,6 +68,7 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida const relevantDetectors = detectors.filter((detector) => { return typeof detector[fieldName] !== 'undefined'; }); + const datafeedConfig = job.datafeed_config; if (relevantDetectors.length > 0) { try { @@ -78,11 +81,26 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida index: job.datafeed_config.indices.join(','), fields: uniqueFieldNames, }); + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); let aggregatableFieldNames: string[] = []; // parse fieldCaps to return an array of just the fields which are aggregatable if (typeof fieldCaps === 'object' && typeof fieldCaps.fields === 'object') { aggregatableFieldNames = uniqueFieldNames.filter((field) => { + if ( + typeof datafeedConfig?.script_fields === 'object' && + datafeedConfig?.script_fields.hasOwnProperty(field) + ) { + return true; + } + // if datafeed has aggregation fields, check recursively if field exist + if ( + datafeedAggregations !== undefined && + isValidAggregationField(datafeedAggregations, field) + ) { + return true; + } + if (typeof fieldCaps.fields[field] !== 'undefined') { const fieldType = Object.keys(fieldCaps.fields[field])[0]; return fieldCaps.fields[field][fieldType].aggregatable; @@ -96,7 +114,10 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida job.datafeed_config.query, aggregatableFieldNames, 0, - job.data_description.time_field + job.data_description.time_field, + undefined, + undefined, + datafeedConfig ); uniqueFieldNames.forEach((uniqueFieldName) => { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts index 6721605355d96..f72885cf223fd 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts @@ -65,7 +65,8 @@ export async function validateModelMemoryLimit( job.data_description.time_field, duration!.start as number, duration!.end as number, - true + true, + job.datafeed_config ); // @ts-expect-error const mmlEstimateBytes: number = numeral(modelMemoryLimit).value(); diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index c11569b8bc1f3..769405c6ef7c2 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { AnalysisConfig } from '../../common/types/anomaly_detection_jobs'; +import { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -35,7 +35,15 @@ export function jobValidationRoutes( mlClient: MlClient, payload: CalculateModelMemoryLimitPayload ) { - const { analysisConfig, indexPattern, query, timeFieldName, earliestMs, latestMs } = payload; + const { + datafeedConfig, + analysisConfig, + indexPattern, + query, + timeFieldName, + earliestMs, + latestMs, + } = payload; return calculateModelMemoryLimitProvider(client, mlClient)( analysisConfig as AnalysisConfig, @@ -43,7 +51,9 @@ export function jobValidationRoutes( query, timeFieldName, earliestMs, - latestMs + latestMs, + undefined, + datafeedConfig as Datafeed ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index ddfb49ce42cb8..f786607e70641 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -20,6 +20,7 @@ export const estimateBucketSpanSchema = schema.object({ }); export const modelMemoryLimitSchema = schema.object({ + datafeedConfig: datafeedConfigSchema, analysisConfig: analysisConfigSchema, indexPattern: schema.string(), query: schema.any(), diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx index b0965f8708558..90ab5c2f888fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -55,7 +55,9 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual('/app/ml/jobs?_a=(queryText:linux_anomalous_network_activity_ecs)') + expect(href).toEqual( + "/app/ml/jobs?_a=(jobs:(queryText:'id:linux_anomalous_network_activity_ecs'))" + ) ); }); @@ -72,7 +74,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual("/app/ml/jobs?_a=(queryText:'job%20id%20with%20spaces')") + expect(href).toEqual("/app/ml/jobs?_a=(jobs:(queryText:'id:job%20id%20with%20spaces'))") ); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ba6ac32f2e3d0..cd45a4f01fc64 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11284,24 +11284,9 @@ "xpack.maps.layerWizardSelect.solutionsCategoryLabel": "ソリューション", "xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "マップを読み込めません", "xpack.maps.map.initializeErrorTitle": "マップを初期化できません", - "xpack.maps.mapListing.advancedSettingsLinkText": "高度な設定", - "xpack.maps.mapListing.cancelTitle": "キャンセル", - "xpack.maps.mapListing.createMapButtonLabel": "マップを作成", - "xpack.maps.mapListing.deleteSelectedButtonLabel": "選択項目を削除", - "xpack.maps.mapListing.deleteSelectedItemsTitle": "選択項目を削除しますか?", - "xpack.maps.mapListing.deleteTitle": "削除", - "xpack.maps.mapListing.deleteWarning": "削除されたアイテムは復元できません。", "xpack.maps.mapListing.descriptionFieldTitle": "説明", "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "マップを読み込めません", - "xpack.maps.mapListing.limitExceededTitle": "リスティング制限超過", - "xpack.maps.mapListing.limitHelpDescription": "{totalItems} 個のアイテムがありますが、listingLimit の設定により {listingLimit} 個までしか下の表に表示できません。この設定は次の場所で変更できます ", - "xpack.maps.mapListing.listingTableTitle": "マップ", - "xpack.maps.mapListing.noItemsDescription": "マップがないようです。作成ボタンをクリックして作成してください。", - "xpack.maps.mapListing.noMatchDescription": "検索に一致するアイテムがありません。", - "xpack.maps.mapListing.searchAriaLabel": "フィルターアイテム", - "xpack.maps.mapListing.searchPlaceholder": "検索…", "xpack.maps.mapListing.titleFieldTitle": "タイトル", - "xpack.maps.mapListing.unableToDeleteToastTitle": "マップを削除できません", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "インデックスパターンを選択", "xpack.maps.mapSavedObjectLabel": "マップ", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "自動的にマップをデータ境界に合わせる", @@ -12191,8 +12176,6 @@ "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "ステータス", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.stats": "統計", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel": "ジョブの詳細", - "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "実験的", - "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "インデックスパターン{indexPattern}が存在するかどうかを確認するときにエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId}は失敗状態です。ジョブを停止して、エラーを修正する必要があります。", "xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "キャンセル", @@ -13205,7 +13188,6 @@ "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "停止したパーティションのリストの取得中にエラーが発生しました。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "パーティション単位の分類とstop_on_warn設定が有効です。ジョブ「{jobId}」の一部のパーティションは分類に適さず、さらなる分類または異常検知分析から除外されました。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "停止したパーティション名", - "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "オプション。インプットデータが事前にまとめられている場合に使用、例: \\{docCountParam\\}。", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "サマリーカウントフィールド", "xpack.ml.newJob.wizard.previewJsonButton": "JSON をプレビュー", "xpack.ml.newJob.wizard.previousStepButton": "前へ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 822ccf5cc8409..97396b09ca6c6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11297,24 +11297,9 @@ "xpack.maps.layerWizardSelect.solutionsCategoryLabel": "解决方案", "xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "无法加载地图", "xpack.maps.map.initializeErrorTitle": "无法初始化地图", - "xpack.maps.mapListing.advancedSettingsLinkText": "高级设置", - "xpack.maps.mapListing.cancelTitle": "取消", - "xpack.maps.mapListing.createMapButtonLabel": "创建地图", - "xpack.maps.mapListing.deleteSelectedButtonLabel": "删除选定", - "xpack.maps.mapListing.deleteSelectedItemsTitle": "删除选定项?", - "xpack.maps.mapListing.deleteTitle": "删除", - "xpack.maps.mapListing.deleteWarning": "您无法恢复已删除项。", "xpack.maps.mapListing.descriptionFieldTitle": "描述", "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "无法加载地图", - "xpack.maps.mapListing.limitExceededTitle": "已超过列表限制", - "xpack.maps.mapListing.limitHelpDescription": "您有 {totalItems} 项,但您的 listingLimit 设置阻止下表显示 {listingLimit} 项以上。此设置可在以下选项下更改: ", - "xpack.maps.mapListing.listingTableTitle": "Maps", - "xpack.maps.mapListing.noItemsDescription": "似乎您没有任何地图。单击创建按钮来创建。", - "xpack.maps.mapListing.noMatchDescription": "没有任何项匹配您的搜索。", - "xpack.maps.mapListing.searchAriaLabel": "筛选项", - "xpack.maps.mapListing.searchPlaceholder": "搜索......", "xpack.maps.mapListing.titleFieldTitle": "标题", - "xpack.maps.mapListing.unableToDeleteToastTitle": "无法删除地图", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "选择索引模式", "xpack.maps.mapSavedObjectLabel": "地图", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "使地图自适应数据边界", @@ -12205,8 +12190,6 @@ "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "状态", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.stats": "统计", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel": "作业详情", - "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "实验性", - "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "检查索引模式 {indexPattern} 是否存在时发生错误:{error}", "xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId} 处于失败状态。您必须停止该作业并修复失败问题。", "xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "取消", @@ -13219,7 +13202,6 @@ "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "提取已停止分区的列表时发生错误。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "启用按分区分类和 stop_on_warn 设置。作业“{jobId}”中的某些分区不适合进行分类,已从进一步分类或异常检测分析中排除。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "已停止的分区名称", - "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "可选,用于输入数据已预汇总时,例如 \\{docCountParam\\}。", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "汇总计数字段", "xpack.ml.newJob.wizard.previewJsonButton": "预览 JSON", "xpack.ml.newJob.wizard.previousStepButton": "上一页", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index e60785f70bffe..f5095101d96b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -63,7 +63,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ fullWidth isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined} name={paramsProperty} - value={inputTargetValue} + value={inputTargetValue || ''} data-test-subj={`${paramsProperty}TextArea`} onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} onFocus={(e: React.FocusEvent) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index fc05b237ccf5e..946bf064eb9ce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -50,7 +50,7 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ id={`${paramsProperty}Id`} isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined} data-test-subj={`${paramsProperty}Input`} - value={inputTargetValue} + value={inputTargetValue || ''} onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} onFocus={(e: React.FocusEvent) => { setCurrentTextElement(e.target); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 00ff6fc132cdc..b53d0816ea068 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -72,10 +72,8 @@ export const ConnectorAddFlyout = ({ const [isSaving, setIsSaving] = useState(false); const closeFlyout = useCallback(() => { - setActionType(undefined); - setConnector(initialConnector); onClose(); - }, [onClose, initialConnector]); + }, [onClose]); const canSave = hasSaveActionsCapability(capabilities); diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index ade78c31211ab..4cea7ddf4854a 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -6,23 +6,19 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; -import { DynamicActionsState, SerializedEvent } from './types'; -import { AdvancedUiActionsServerPlugin } from './plugin'; +import { ActionFactory, DynamicActionsState, SerializedEvent } from './types'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; export const dynamicActionEnhancement = ( - uiActionsEnhanced: AdvancedUiActionsServerPlugin + getActionFactory: (id: string) => undefined | ActionFactory ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', telemetry: (state: SerializableState, telemetry: Record) => { let telemetryData = telemetry; (state as DynamicActionsState).events.forEach((event: SerializedEvent) => { - if (uiActionsEnhanced.getActionFactory(event.action.factoryId)) { - telemetryData = uiActionsEnhanced - .getActionFactory(event.action.factoryId)! - .telemetry(event, telemetryData); - } + const factory = getActionFactory(event.action.factoryId); + if (factory) telemetryData = factory.telemetry(event, telemetryData); }); return telemetryData; }, @@ -30,8 +26,9 @@ export const dynamicActionEnhancement = ( const references: SavedObjectReference[] = []; const newState: DynamicActionsState = { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { - const result = uiActionsEnhanced.getActionFactory(event.action.factoryId) - ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.extract(event) + const factory = getActionFactory(event.action.factoryId); + const result = factory + ? factory.extract(event) : { state: event, references: [], @@ -45,9 +42,8 @@ export const dynamicActionEnhancement = ( inject: (state: SerializableState, references: SavedObjectReference[]) => { return { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { - return uiActionsEnhanced.getActionFactory(event.action.factoryId) - ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.inject(event, references) - : event; + const factory = getActionFactory(event.action.factoryId); + return factory ? factory.inject(event, references) : event; }), } as DynamicActionsState; }, diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index 718304018730d..e6362418efc66 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -5,15 +5,10 @@ */ import { identity } from 'lodash'; -import { CoreSetup, Plugin, SavedObjectReference } from '../../../../src/core/server'; +import { CoreSetup, Plugin } from '../../../../src/core/server'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; import { dynamicActionEnhancement } from './dynamic_action_enhancement'; -import { - ActionFactoryRegistry, - SerializedEvent, - ActionFactoryDefinition, - DynamicActionsState, -} from './types'; +import { ActionFactoryRegistry, SerializedEvent, ActionFactoryDefinition } from './types'; export interface SetupContract { registerActionFactory: (definition: ActionFactoryDefinition) => void; @@ -32,7 +27,9 @@ export class AdvancedUiActionsServerPlugin constructor() {} public setup(core: CoreSetup, { embeddable }: SetupDependencies) { - embeddable.registerEnhancement(dynamicActionEnhancement(this)); + const getActionFactory = (actionFactoryId: string) => this.actionFactories.get(actionFactoryId); + + embeddable.registerEnhancement(dynamicActionEnhancement(getActionFactory)); return { registerActionFactory: this.registerActionFactory, @@ -64,45 +61,4 @@ export class AdvancedUiActionsServerPlugin migrations: definition.migrations || {}, }); }; - - public readonly getActionFactory = (actionFactoryId: string) => { - const actionFactory = this.actionFactories.get(actionFactoryId); - return actionFactory; - }; - - public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => { - state.events.forEach((event: SerializedEvent) => { - if (this.actionFactories.has(event.action.factoryId)) { - this.actionFactories.get(event.action.factoryId)!.telemetry(event, telemetry); - } - }); - return telemetry; - }; - - public readonly extract = (state: DynamicActionsState) => { - const references: SavedObjectReference[] = []; - const newState = { - events: state.events.map((event: SerializedEvent) => { - const result = this.actionFactories.has(event.action.factoryId) - ? this.actionFactories.get(event.action.factoryId)!.extract(event) - : { - state: event, - references: [], - }; - result.references.forEach((r) => references.push(r)); - return result.state; - }), - }; - return { state: newState, references }; - }; - - public readonly inject = (state: DynamicActionsState, references: SavedObjectReference[]) => { - return { - events: state.events.map((event: SerializedEvent) => { - return this.actionFactories.has(event.action.factoryId) - ? this.actionFactories.get(event.action.factoryId)!.inject(event, references) - : event; - }), - }; - }; } diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index 86e5f2876ca28..427061b6c16d4 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -7,8 +7,13 @@ import { getPingHistogram } from '../get_ping_histogram'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +import * as intervalHelper from '../../helper/get_histogram_interval'; describe('getPingHistogram', () => { + beforeEach(() => { + jest.spyOn(intervalHelper, 'getHistogramInterval').mockReturnValue(36000); + }); + const standardMockResponse: any = { aggregations: { timeseries: { @@ -36,7 +41,7 @@ describe('getPingHistogram', () => { }, }; - it.skip('returns a single bucket if array has 1', async () => { + it('returns a single bucket if array has 1', async () => { expect.assertions(2); const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 020fcf5331188..2ff1043d79e84 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -151,7 +151,7 @@ export const getHistogramForMonitors = async ( }, }, }; - const result = await queryContext.search(params); + const { body: result } = await queryContext.search(params); const histoBuckets: any[] = result.aggregations?.histogram.buckets ?? []; const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index f9fdfaed1c79b..cb78e76bdd697 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -92,6 +92,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + '--xpack.alerts.invalidateApiKeysTask.interval="15s"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, ...actionsProxyUrl, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 7ed864afac4cc..998ec6ab2ed0e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -437,6 +437,21 @@ export function defineAlertTypes( throw new Error('this alert is intended to fail'); }, }; + const longRunningAlertType: AlertType = { + id: 'test.longRunning', + name: 'Test: Long Running', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alertsFixture', + defaultActionGroupId: 'default', + async executor() { + await new Promise((resolve) => setTimeout(resolve, 5000)); + }, + }; alerts.registerType(getAlwaysFiringAlertType()); alerts.registerType(getCumulativeFiringAlertType()); @@ -449,4 +464,5 @@ export function defineAlertTypes( alerts.registerType(onlyStateVariablesAlertType); alerts.registerType(getPatternFiringAlertType()); alerts.registerType(throwAlertType); + alerts.registerType(longRunningAlertType); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index fbf3b798500d3..d832902fe066d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -50,6 +50,7 @@ export class FixturePlugin implements Plugin, + res: KibanaResponseFactory + ): Promise> => { + try { + const [{ savedObjects }] = await core.getStartServices(); + const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, { + includedHiddenTypes: ['api_key_pending_invalidation'], + }); + const findResult = await savedObjectsWithTasksAndAlerts.find({ + type: 'api_key_pending_invalidation', + }); + return res.ok({ + body: { apiKeysToInvalidate: findResult.saved_objects }, + }); + } catch (err) { + return res.badRequest({ body: err }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 8836bc2e4db2f..9c3d2801c0886 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -836,6 +836,80 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); + it('should handle updates for a long running alert type without failing the underlying tasks due to invalidated ApiKey', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send({ + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.longRunning', + consumer: 'alertsFixture', + schedule: { interval: '1s' }, + throttle: '1m', + actions: [], + params: {}, + }) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '1m' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + const statusUpdates: string[] = []; + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + statusUpdates.push(alertTask.status); + expect(alertTask.status).to.eql('idle'); + }); + + expect(statusUpdates.find((status) => status === 'failed')).to.be(undefined); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.longRunning', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + expect(alertTask.status).to.eql('idle'); + // ensure the alert is rescheduled to a minute from now + ensureDatetimeIsWithinRange(Date.parse(alertTask.runAt), 60 * 1000); + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should handle updates to an alert schedule by setting the new schedule for the underlying task', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index e4e6adca9640f..cb663115b958b 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -303,6 +303,12 @@ export default ({ getService }: FtrProviderContext) => { url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#model-memory-limits`, status: 'warning', }, + { + id: 'missing_summary_count_field_name', + status: 'error', + text: + 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', + }, ]; expect(body.length).to.eql( diff --git a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap new file mode 100644 index 0000000000000..50625683b605d --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap @@ -0,0 +1,369 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`monitor states endpoint will fetch monitor state data for the given down filters 1`] = ` +Object { + "nextPagePagination": "{\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\",\\"cursorKey\\":{\\"monitor_id\\":\\"0020-down\\"}}", + "prevPagePagination": null, + "summaries": Array [ + Object { + "histogram": Object { + "points": Array [ + Object { + "down": 1, + "timestamp": 1568172624744, + }, + Object { + "down": 2, + "timestamp": 1568172677247, + }, + Object { + "down": 1, + "timestamp": 1568172729750, + }, + Object { + "down": 2, + "timestamp": 1568172782253, + }, + Object { + "down": 2, + "timestamp": 1568172834756, + }, + Object { + "down": 2, + "timestamp": 1568172887259, + }, + Object { + "down": 1, + "timestamp": 1568172939762, + }, + Object { + "down": 2, + "timestamp": 1568172992265, + }, + Object { + "down": 2, + "timestamp": 1568173044768, + }, + Object { + "down": 2, + "timestamp": 1568173097271, + }, + Object { + "down": 1, + "timestamp": 1568173149774, + }, + Object { + "down": 2, + "timestamp": 1568173202277, + }, + ], + }, + "minInterval": 52503, + "monitor_id": "0010-down", + "state": Object { + "monitor": Object { + "name": "", + }, + "observer": Object { + "geo": Object { + "name": Array [ + "mpls", + ], + }, + }, + "summary": Object { + "down": 1, + "status": "down", + "up": 0, + }, + "summaryPings": Array [ + Object { + "@timestamp": "2019-09-11T03:40:34.371Z", + "agent": Object { + "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", + "hostname": "avc-x1x", + "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", + "type": "heartbeat", + "version": "8.0.0", + }, + "docId": "rZtoHm0B0I9WX_CznN_V", + "ecs": Object { + "version": "1.1.0", + }, + "error": Object { + "message": "400 Bad Request", + "type": "validate", + }, + "event": Object { + "dataset": "uptime", + }, + "host": Object { + "name": "avc-x1x", + }, + "http": Object { + "response": Object { + "body": Object { + "bytes": 3, + "content": "400", + "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", + }, + "status_code": 400, + }, + "rtt": Object { + "content": Object { + "us": 41, + }, + "response_header": Object { + "us": 36777, + }, + "total": Object { + "us": 37821, + }, + "validate": Object { + "us": 36818, + }, + "write_request": Object { + "us": 53, + }, + }, + }, + "monitor": Object { + "check_group": "d76f07d1-d445-11e9-88e3-3e80641b9c71", + "duration": Object { + "us": 37926, + }, + "id": "0010-down", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "http", + }, + "observer": Object { + "geo": Object { + "location": "37.926868, -78.024902", + "name": "mpls", + }, + "hostname": "avc-x1x", + }, + "resolve": Object { + "ip": "127.0.0.1", + "rtt": Object { + "us": 56, + }, + }, + "summary": Object { + "down": 1, + "up": 0, + }, + "tcp": Object { + "rtt": Object { + "connect": Object { + "us": 890, + }, + }, + }, + "timestamp": "2019-09-11T03:40:34.371Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + ], + "timestamp": "2019-09-11T03:40:34.371Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + }, + Object { + "histogram": Object { + "points": Array [ + Object { + "down": 1, + "timestamp": 1568172624744, + }, + Object { + "down": 2, + "timestamp": 1568172677247, + }, + Object { + "down": 1, + "timestamp": 1568172729750, + }, + Object { + "down": 2, + "timestamp": 1568172782253, + }, + Object { + "down": 2, + "timestamp": 1568172834756, + }, + Object { + "down": 2, + "timestamp": 1568172887259, + }, + Object { + "down": 1, + "timestamp": 1568172939762, + }, + Object { + "down": 2, + "timestamp": 1568172992265, + }, + Object { + "down": 2, + "timestamp": 1568173044768, + }, + Object { + "down": 2, + "timestamp": 1568173097271, + }, + Object { + "down": 1, + "timestamp": 1568173149774, + }, + Object { + "down": 2, + "timestamp": 1568173202277, + }, + ], + }, + "minInterval": 52503, + "monitor_id": "0020-down", + "state": Object { + "monitor": Object { + "name": "", + }, + "observer": Object { + "geo": Object { + "name": Array [ + "mpls", + ], + }, + }, + "summary": Object { + "down": 1, + "status": "down", + "up": 0, + }, + "summaryPings": Array [ + Object { + "@timestamp": "2019-09-11T03:40:34.372Z", + "agent": Object { + "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", + "hostname": "avc-x1x", + "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", + "type": "heartbeat", + "version": "8.0.0", + }, + "docId": "X5toHm0B0I9WX_CznN-6", + "ecs": Object { + "version": "1.1.0", + }, + "error": Object { + "message": "400 Bad Request", + "type": "validate", + }, + "event": Object { + "dataset": "uptime", + }, + "host": Object { + "name": "avc-x1x", + }, + "http": Object { + "response": Object { + "body": Object { + "bytes": 3, + "content": "400", + "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", + }, + "status_code": 400, + }, + "rtt": Object { + "content": Object { + "us": 54, + }, + "response_header": Object { + "us": 180, + }, + "total": Object { + "us": 555, + }, + "validate": Object { + "us": 234, + }, + "write_request": Object { + "us": 63, + }, + }, + }, + "monitor": Object { + "check_group": "d7712ecb-d445-11e9-88e3-3e80641b9c71", + "duration": Object { + "us": 14900, + }, + "id": "0020-down", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "http", + }, + "observer": Object { + "geo": Object { + "location": "37.926868, -78.024902", + "name": "mpls", + }, + "hostname": "avc-x1x", + }, + "resolve": Object { + "ip": "127.0.0.1", + "rtt": Object { + "us": 14294, + }, + }, + "summary": Object { + "down": 1, + "up": 0, + }, + "tcp": Object { + "rtt": Object { + "connect": Object { + "us": 105, + }, + }, + }, + "timestamp": "2019-09-11T03:40:34.372Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + ], + "timestamp": "2019-09-11T03:40:34.372Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + }, + ], + "totalSummaryCount": 2000, +} +`; diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index f59b79a6b7bfc..6f410add0fa4d 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -9,12 +9,15 @@ import { settingsObjectId, settingsObjectType, } from '../../../../../plugins/uptime/server/lib/saved_objects'; +import { registerMochaHooksForSnapshots } from '../../../../apm_api_integration/common/match_snapshot'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const server = getService('kibanaServer'); describe('uptime REST endpoints', () => { + registerMochaHooksForSnapshots(); + beforeEach('clear settings', async () => { try { await server.savedObjects.delete({ diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index d3c49bb49ff52..08a339ed59326 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -9,6 +9,7 @@ import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { MonitorSummariesResultType } from '../../../../../plugins/uptime/common/runtime_types'; import { API_URLS } from '../../../../../plugins/uptime/common/constants'; +import { expectSnapshot } from '../../../../apm_api_integration/common/match_snapshot'; interface ExpectedMonitorStatesPage { response: any; @@ -90,6 +91,16 @@ export default function ({ getService }: FtrProviderContext) { }); }); + it('will fetch monitor state data for the given down filters', async () => { + const statusFilter = 'down'; + const size = 2; + const { body } = await supertest.get( + `${API_URLS.MONITOR_LIST}?dateRangeStart=${from}&dateRangeEnd=${to}&statusFilter=${statusFilter}&pageSize=${size}` + ); + + expectSnapshot(body).toMatch(); + }); + it('can navigate forward and backward using pagination', async () => { const expectedResultsCount = 100; const size = 10; diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index b5a7935a81eb5..6d86b93c3ec44 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }) { ['global_maps_all', 'test_logstash_reader', 'geoshape_data_reader'], false ); - await PageObjects.maps.loadSavedMap('geo grid vector grid example (SUPER_FINE resolution)'); + await PageObjects.maps.loadSavedMap('geo grid vector grid example SUPER_FINE resolution'); }); after(async () => { diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/saved_object_management.js index 277a8a5651453..8c62136472921 100644 --- a/x-pack/test/functional/apps/maps/saved_object_management.js +++ b/x-pack/test/functional/apps/maps/saved_object_management.js @@ -139,8 +139,8 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.openNewMap(); await PageObjects.maps.saveMap(MAP1_NAME); - const count = await PageObjects.maps.getMapCountWithName(MAP1_NAME); - expect(count).to.equal(1); + + await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 1); }); it('should allow saving map that crosses dateline', async () => { @@ -148,8 +148,8 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.setView('64', '179', '5'); await PageObjects.maps.saveMap(MAP2_NAME); - const count = await PageObjects.maps.getMapCountWithName(MAP2_NAME); - expect(count).to.equal(1); + + await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 1); }); }); @@ -157,11 +157,9 @@ export default function ({ getPageObjects, getService }) { it('should delete selected saved objects', async () => { await PageObjects.maps.deleteSavedMaps(MAP_NAME_PREFIX); - const map1Count = await PageObjects.maps.getMapCountWithName(MAP1_NAME); - expect(map1Count).to.equal(0); + await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 0); - const map2Count = await PageObjects.maps.getMapCountWithName(MAP2_NAME); - expect(map2Count).to.equal(0); + await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 0); }); }); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts new file mode 100644 index 0000000000000..ff2bbd077fa8f --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; +import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('total feature importance panel and decision path popover', function () { + const testDataList: Array<{ + suiteTitle: string; + archive: string; + indexPattern: { name: string; timeField: string }; + job: DeepPartial; + }> = (() => { + const timestamp = Date.now(); + + return [ + { + suiteTitle: 'binary classification job', + archive: 'ml/ihp_outlier', + indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' }, + job: { + id: `ihp_fi_binary_${timestamp}`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'", + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-ihp_fi_binary_${timestamp}`; + }, + results_field: 'ml_central_air', + }, + analyzed_fields: { + includes: [ + 'CentralAir', + 'GarageArea', + 'GarageCars', + 'YearBuilt', + 'Electrical', + 'Neighborhood', + 'Heating', + '1stFlrSF', + ], + }, + analysis: { + classification: { + dependent_variable: 'CentralAir', + num_top_feature_importance_values: 5, + training_percent: 35, + prediction_field_name: 'CentralAir_prediction', + num_top_classes: -1, + }, + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + }, + { + suiteTitle: 'multi class classification job', + archive: 'ml/ihp_outlier', + indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' }, + job: { + id: `ihp_fi_multi_${timestamp}`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'", + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-ihp_fi_multi_${timestamp}`; + }, + results_field: 'ml_heating_qc', + }, + analyzed_fields: { + includes: [ + 'CentralAir', + 'GarageArea', + 'GarageCars', + 'Electrical', + 'Neighborhood', + 'Heating', + '1stFlrSF', + 'HeatingQC', + ], + }, + analysis: { + classification: { + dependent_variable: 'HeatingQC', + num_top_feature_importance_values: 5, + training_percent: 35, + prediction_field_name: 'heatingqc', + num_top_classes: -1, + }, + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + }, + { + suiteTitle: 'regression job', + archive: 'ml/egs_regression', + indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' }, + job: { + id: `egs_fi_reg_${timestamp}`, + description: 'This is the job description', + source: { + index: ['ft_egs_regression'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-egs_fi_reg_${timestamp}`; + }, + results_field: 'ml', + }, + analysis: { + regression: { + prediction_field_name: 'test', + dependent_variable: 'stab', + num_top_feature_importance_values: 5, + training_percent: 35, + }, + }, + analyzed_fields: { + includes: [ + 'g1', + 'g2', + 'g3', + 'g4', + 'p1', + 'p2', + 'p3', + 'p4', + 'stab', + 'tau1', + 'tau2', + 'tau3', + 'tau4', + ], + excludes: [], + }, + model_memory_limit: '20mb', + }, + }, + ]; + })(); + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + for (const testData of testDataList) { + await esArchiver.loadIfNeeded(testData.archive); + await ml.testResources.createIndexPatternIfNeeded( + testData.indexPattern.name, + testData.indexPattern.timeField + ); + await ml.api.createAndRunDFAJob(testData.job as DataFrameAnalyticsConfig); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + before(async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataFrameAnalytics(); + await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.job.id as string); + }); + + after(async () => { + await ml.api.deleteIndices(testData.job.dest!.index as string); + await ml.testResources.deleteIndexPatternByTitle(testData.job.dest!.index as string); + }); + + it('should display the total feature importance in the results view', async () => { + await ml.dataFrameAnalyticsResults.assertTotalFeatureImportanceEvaluatePanelExists(); + }); + + it('should display the feature importance decision path in the data grid', async () => { + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + await ml.dataFrameAnalyticsResults.openFeatureImportanceDecisionPathPopover(); + await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathElementsExists(); + await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathChartElementsExists(); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts index 0202c8431ce34..a57d26b536b4f 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./regression_creation')); loadTestFile(require.resolve('./classification_creation')); loadTestFile(require.resolve('./cloning')); + loadTestFile(require.resolve('./feature_importance')); }); } diff --git a/x-pack/test/functional/es_archives/discover/default/mappings.json b/x-pack/test/functional/es_archives/discover/default/mappings.json index 82002c095bcc5..53bbe8a5baa5b 100644 --- a/x-pack/test/functional/es_archives/discover/default/mappings.json +++ b/x-pack/test/functional/es_archives/discover/default/mappings.json @@ -93,6 +93,9 @@ }, "title": { "type": "text" + }, + "fieldAttrs": { + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index e3a8743e60897..79e8c14cc3982 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -638,7 +638,7 @@ "description": "", "layerListJSON": "[{\"id\":\"g1xkv\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"resolution\": \"SUPER_FINE\",\"type\":\"ES_GEO_GRID\",\"id\":\"9305f6ea-4518-4c06-95b9-33321aa38d6a\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"metrics\":[{\"type\":\"count\"},{\"type\":\"max\",\"field\":\"bytes\"}]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max of bytes\",\"name\":\"max_of_bytes\",\"origin\":\"source\"},\"color\":\"Blues\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#cccccc\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"TILED_VECTOR\"}]", "mapStateJSON": "{\"zoom\":3.59,\"center\":{\"lon\":-98.05765,\"lat\":38.32288},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", - "title": "geo grid vector grid example (SUPER_FINE resolution)", + "title": "geo grid vector grid example SUPER_FINE resolution", "uiStateJSON": "{\"isDarkMode\":false}" }, "type": "map" diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index c4f1bd7dc2a6b..7e22acf785d36 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -21,6 +21,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const renderable = getService('renderable'); const browser = getService('browser'); const MenuToggle = getService('MenuToggle'); + const listingTable = getService('listingTable'); const setViewPopoverToggle = new MenuToggle({ name: 'SetView Popover', @@ -120,13 +121,10 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await retry.try(async () => { await this.searchForMapWithName(name); - await this.selectMap(name); + await listingTable.clickItemLink('map', name); await PageObjects.header.waitUntilLoadingHasFinished(); - - const onMapListingPage = await this.onMapListingPage(); - if (onMapListingPage) { - throw new Error(`Failed to open map ${name}`); - } + // check Map landing page is not present + await testSubjects.missingOrFail('mapLandingPage', { timeout: 10000 }); }); await this.waitForLayersToLoad(); @@ -134,8 +132,8 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte async deleteSavedMaps(search: string) { await this.searchForMapWithName(search); - await testSubjects.click('checkboxSelectAll'); - await testSubjects.click('deleteSelectedItems'); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); await PageObjects.common.clickConfirmOnModal(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -150,7 +148,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await renderable.waitForRender(); } - async saveMap(name: string, uncheckReturnToOriginModeSwitch = false) { + async saveMap(name: string, uncheckReturnToOriginModeSwitch = false, tags?: string[]) { await testSubjects.click('mapSaveButton'); await testSubjects.setValue('savedObjectTitle', name); if (uncheckReturnToOriginModeSwitch) { @@ -162,6 +160,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } await testSubjects.setEuiSwitch('returnToOriginModeSwitch', 'uncheck'); } + if (tags) { + await testSubjects.click('savedObjectTagSelector'); + for (const tagName of tags) { + await testSubjects.click(`tagSelectorOption-${tagName.replace(' ', '_')}`); + } + await testSubjects.click('savedObjectTitle'); + } await testSubjects.clickWhenNotDisabled('confirmSaveSavedObjectButton'); } @@ -174,7 +179,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } async expectMissingCreateNewButton() { - await testSubjects.missingOrFail('newMapLink'); + await testSubjects.missingOrFail('newItemButton'); } async expectMissingAddLayerButton() { @@ -187,8 +192,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte async onMapListingPage() { log.debug(`onMapListingPage`); - const exists = await testSubjects.exists('mapsListingPage', { timeout: 3500 }); - return exists; + return await listingTable.onListingPage('map'); } async searchForMapWithName(name: string) { @@ -196,21 +200,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await this.gotoMapListingPage(); - await retry.try(async () => { - const searchFilter = await testSubjects.find('searchFilter'); - await searchFilter.clearValue(); - await searchFilter.click(); - await searchFilter.type(name); - await PageObjects.common.pressEnterKey(); - }); + await listingTable.searchForItemWithName(name); await PageObjects.header.waitUntilLoadingHasFinished(); } - async selectMap(name: string) { - await testSubjects.click(`mapListingTitleLink-${name.split(' ').join('-')}`); - } - async getHits() { await inspector.open(); await inspector.openInspectorRequestsView(); @@ -232,13 +226,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } } - async getMapCountWithName(name: string) { + async searchAndExpectItemsCount(name: string, count: number) { await this.gotoMapListingPage(); - log.debug(`getMapCountWithName: ${name}`); - await this.searchForMapWithName(name); - const buttons = await find.allByButtonText(name); - return buttons.length; + log.debug(`searchAndExpectItemsCount: ${name}`); + await listingTable.searchAndExpectItemsCount('map', name, count); } async setView(lat: number, lon: number, zoom: number) { diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index 8781a2cd216f2..1ac11a0149897 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -5,12 +5,14 @@ */ import expect from '@kbn/expect'; +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataFrameAnalyticsResultsProvider({ getService, }: FtrProviderContext) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return { @@ -60,5 +62,59 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({ `DFA results table should have at least one row (got '${resultTableRows.length}')` ); }, + + async assertTotalFeatureImportanceEvaluatePanelExists() { + await testSubjects.existOrFail('mlDFExpandableSection-FeatureImportanceSummary'); + await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 5000 }); + }, + + async assertFeatureImportanceDecisionPathElementsExists() { + await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_chart', { + timeout: 5000, + }); + await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_json', { + timeout: 5000, + }); + }, + + async assertFeatureImportanceDecisionPathChartElementsExists() { + await testSubjects.existOrFail('mlDFADecisionPathChart', { + timeout: 5000, + }); + }, + + async openFeatureImportanceDecisionPathPopover() { + this.assertResultsTableNotEmpty(); + + const featureImportanceCell = await this.getFirstFeatureImportanceCell(); + const interactionButton = await featureImportanceCell.findByTagName('button'); + + // simulate hover and wait for button to appear + await featureImportanceCell.moveMouseTo(); + await this.waitForInteractionButtonToDisplay(interactionButton); + + // open popover + await interactionButton.click(); + await testSubjects.existOrFail('mlDFADecisionPathPopover'); + }, + + async getFirstFeatureImportanceCell(): Promise { + // get first row of the data grid + const firstDataGridRow = await testSubjects.find( + 'mlExplorationDataGrid loaded > dataGridRow' + ); + // find the feature importance cell in that row + const featureImportanceCell = await firstDataGridRow.findByCssSelector( + '[data-test-subj="dataGridRowCell"][class*="featureImportance"]' + ); + return featureImportanceCell; + }, + + async waitForInteractionButtonToDisplay(interactionButton: WebElementWrapper) { + await retry.tryForTime(5000, async () => { + const buttonVisible = await interactionButton.isDisplayed(); + expect(buttonVisible).to.equal(true, 'Expected data grid cell button to be visible'); + }); + }, }; } diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts index 4f08134365e95..8734b7cf5bb68 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts index 70884ba6c968b..8ca92ac472c6e 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts index 64f120fd75629..a2e3630622d67 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts @@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts index 1a354bbbcb660..9cde766b4f514 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts @@ -57,6 +57,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts index 61b859cf81992..677bdee56ed8b 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts @@ -63,6 +63,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts index 77bf9d7ca3287..3347eca9920d6 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json new file mode 100644 index 0000000000000..cdaf4fe171ec0 --- /dev/null +++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json @@ -0,0 +1,210 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "_reserved": true, + "description": "This is the default space", + "name": "Default Space" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-1", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-1", + "description": "My first tag!", + "color": "#FF00FF" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-2", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-2", + "description": "Another awesome tag", + "color": "#11FF22" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-3", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-3", + "description": "Last but not least", + "color": "#AA0077" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:6.3.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-04-11T20:43:55.434Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:63af0ed0-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map": { + "title" : "map 3 (tag-1 and tag-3)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"d897b506-e719-42b8-9927-351eedd7d357\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-1", + "name" : "tag-ref-tag-1" + }, + { + "type" : "tag", + "id" : "tag-3", + "name" : "tag-ref-tag-3" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:32:16.189Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:4afc6d10-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 1 (tag-2)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"6a91fb66-465c-4193-8c59-9b3f5f262756\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":16.22097},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-2", + "name" : "tag-ref-tag-2" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:31:34.753Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:562cce50-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 2 (tag-3)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"285d5190-aaf1-4dfc-912b-9c7d9e0104a8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-3", + "name" : "tag-ref-tag-3" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:31:53.525Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:6f021340-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 4 (tag-1)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"3deeb666-33cf-4e9a-ab78-e453ed9d721d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-1", + "name" : "tag-ref-tag-1" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:32:35.188Z" + } + } +} diff --git a/x-pack/test/saved_object_tagging/common/lib/authentication.ts b/x-pack/test/saved_object_tagging/common/lib/authentication.ts index c318755bedcdd..8917057ad685e 100644 --- a/x-pack/test/saved_object_tagging/common/lib/authentication.ts +++ b/x-pack/test/saved_object_tagging/common/lib/authentication.ts @@ -118,6 +118,19 @@ export const ROLES = { ], }, }, + KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER: { + name: 'kibana_rbac_default_space_maps_read_user', + privileges: { + kibana: [ + { + feature: { + maps: ['read'], + }, + spaces: ['default'], + }, + ], + }, + }, }; export const USERS = { @@ -185,4 +198,9 @@ export const USERS = { password: 'password', roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER.name], }, + DEFAULT_SPACE_MAPS_READ_USER: { + username: 'a_kibana_rbac_default_space_maps_read_user', + password: 'password', + roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER.name], + }, }; diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 43673487ba74f..0ddfa64d682a8 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./visualize_integration')); loadTestFile(require.resolve('./dashboard_integration')); loadTestFile(require.resolve('./feature_control')); + loadTestFile(require.resolve('./maps_integration')); }); } diff --git a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts new file mode 100644 index 0000000000000..4e44659b4fc67 --- /dev/null +++ b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const PageObjects = getPageObjects(['maps', 'tagManagement', 'common']); + + /** + * Select tags in the searchbar's tag filter. + */ + const selectFilterTags = async (...tagNames: string[]) => { + // open the filter dropdown + const filterButton = await find.byCssSelector('.euiFilterGroup .euiFilterButton'); + await filterButton.click(); + // select the tags + for (const tagName of tagNames) { + await testSubjects.click( + `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(tagName)}` + ); + } + // click elsewhere to close the filter dropdown + const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + await searchFilter.click(); + }; + + describe('maps integration', () => { + before(async () => { + await esArchiver.load('maps'); + }); + after(async () => { + await esArchiver.unload('maps'); + }); + + describe('listing', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/'); + await PageObjects.maps.gotoMapListingPage(); + }); + + it('allows to manually type tag filter query', async () => { + await listingTable.searchForItemWithName('tag:(tag-1)', { escape: false }); + + await listingTable.expectItemsCount('map', 2); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 4 (tag-1)']); + }); + + it('allows to filter by selecting a tag in the filter menu', async () => { + await selectFilterTags('tag-3'); + + await listingTable.expectItemsCount('map', 2); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 2 (tag-3)']); + }); + + it('allows to filter by multiple tags', async () => { + await selectFilterTags('tag-2', 'tag-3'); + + await listingTable.expectItemsCount('map', 3); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 1 (tag-2)', 'map 2 (tag-3)']); + }); + }); + + describe('creating', () => { + beforeEach(async () => { + await PageObjects.maps.openNewMap(); + }); + + it('allows to select tags for a new map', async () => { + await PageObjects.maps.saveMap('my-new-map', false, ['tag-1', 'tag-3']); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('tag-1'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('my-new-map'); + }); + + it('allows to create a tag from the tag selector', async () => { + const { tagModal } = PageObjects.tagManagement; + + await testSubjects.click('mapSaveButton'); + await testSubjects.setValue('savedObjectTitle', 'map-with-new-tag'); + + await testSubjects.click('savedObjectTagSelector'); + await testSubjects.click(`tagSelectorOption-action__create`); + + expect(await tagModal.isOpened()).to.be(true); + + await tagModal.fillForm( + { + name: 'my-new-tag', + color: '#FFCC33', + description: '', + }, + { + submit: true, + } + ); + + expect(await tagModal.isOpened()).to.be(false); + + await testSubjects.click('confirmSaveSavedObjectButton'); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('my-new-tag'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('map-with-new-tag'); + }); + }); + + describe('editing', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/'); + }); + + it('allows to select tags for an existing map', async () => { + await listingTable.clickItemLink('map', 'map 4 (tag-1)'); + + await PageObjects.maps.saveMap('map 4 (tag-1)', false, ['tag-3']); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('tag-3'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('map 4 (tag-1)'); + }); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index 3497fdf83d7dd..91ae4b236adf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5277,10 +5277,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== -"@types/nodemailer@^6.2.1": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.2.1.tgz#8f089bf0ef826f04b9d8dd8750233b04978cb675" - integrity sha512-6f46rxxaFwyOW39psPoQiM7jHjL7apDRNT5WPHIuv+TZFv+7sBGSI9J7blIC3/NWff4O9/VSzgoQtO6aPLUdvQ== +"@types/nodemailer@^6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.0.tgz#d8c039be3ed685c4719a026455555be82c124b74" + integrity sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA== dependencies: "@types/node" "*" @@ -20791,10 +20791,10 @@ node-status-codes@^1.0.0: resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8= -nodemailer@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.7.0.tgz#4420e06abfffd77d0618f184ea49047db84f4ad8" - integrity sha512-IludxDypFpYw4xpzKdMAozBSkzKHmNBvGanUREjJItgJ2NYcK/s8+PggVhj7c2yGFQykKsnnmv1+Aqo0ZfjHmw== +nodemailer@^6.4.16: + version "6.4.16" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.16.tgz#5cb6391b1d79ab7eff32d6f9f48366b5a7117293" + integrity sha512-68K0LgZ6hmZ7PVmwL78gzNdjpj5viqBdFqKrTtr9bZbJYj6BRj5W6WGkxXrEnUl3Co3CBXi3CZBUlpV/foGnOQ== nodemon@^2.0.4: version "2.0.6"