diff --git a/.eslintrc.js b/.eslintrc.js index a2b8ae7622d0b..dfb4603ba95af 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -96,7 +96,7 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], rules: { 'jsx-a11y/click-events-have-key-events': 'off', }, @@ -185,31 +185,40 @@ module.exports = { zones: [ { target: [ - 'src/legacy/**/*', - 'x-pack/**/*', - '!x-pack/**/*.test.*', - '!x-pack/test/**/*', + '(src|x-pack)/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', - 'src/core/(public|server)/**/*', 'examples/**/*', ], from: [ 'src/core/public/**/*', - '!src/core/public/index.ts', - '!src/core/public/mocks.ts', - '!src/core/public/*.test.mocks.ts', + '!src/core/public/index.ts', // relative import + '!src/core/public/mocks{,.ts}', + '!src/core/server/types{,.ts}', '!src/core/public/utils/**/*', + '!src/core/public/*.test.mocks{,.ts}', 'src/core/server/**/*', - '!src/core/server/index.ts', - '!src/core/server/mocks.ts', - '!src/core/server/types.ts', - '!src/core/server/test_utils.ts', + '!src/core/server/index.ts', // relative import + '!src/core/server/mocks{,.ts}', + '!src/core/server/types{,.ts}', + '!src/core/server/test_utils', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 - '!src/core/server/types', - '!src/core/server/*.test.mocks.ts', - + '!src/core/server/*.test.mocks{,.ts}', + ], + allowSameFolder: true, + errorMessage: + 'Plugins may only import from top-level public and server modules in core.', + }, + { + target: [ + '(src|x-pack)/legacy/**/*', + '(src|x-pack)/plugins/**/(public|server)/**/*', + 'examples/**/*', + '!(src|x-pack)/**/*.test.*', + '!(x-pack/)?test/**/*', + ], + from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', ], @@ -730,6 +739,19 @@ module.exports = { }, }, + /** + * Alerting Services overrides + */ + { + // typescript only for front and back end + files: [ + 'x-pack/{,legacy/}plugins/{alerting,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', + ], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + /** * Lens overrides */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ab05b32ab063e..0d86726dca836 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,10 +13,12 @@ /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app /src/plugins/vis_type_xy/ @elastic/kibana-app +/src/plugins/vis_type_table/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/vis_type_timelion/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app +/src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/vis_type_timeseries/ @elastic/kibana-app /src/plugins/vis_type_metric/ @elastic/kibana-app diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 544dd577313df..89e0af270c54d 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -8,6 +8,9 @@ - "Feature:ExpressionLanguage": - "src/plugins/expressions/**/*.*" - "src/plugins/bfetch/**/*.*" + - "Team:apm" + - "x-pack/plugins/apm/**/*.*" + - "x-pack/legacy/plugins/apm/**/*.*" - "Team:uptime": - "x-pack/plugins/uptime/**/*.*" - "x-pack/legacy/plugins/uptime/**/*.*" diff --git a/.i18nrc.json b/.i18nrc.json index 4a516f23ebf05..35ce745234346 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -14,7 +14,7 @@ "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", - "inputControl": "src/legacy/core_plugins/input_control_vis", + "inputControl": "src/plugins/input_control_vis", "inspector": "src/plugins/inspector", "inspectorViews": "src/legacy/core_plugins/inspector_views", "interpreter": "src/legacy/core_plugins/interpreter", @@ -48,10 +48,10 @@ "visDefaultEditor": "src/plugins/vis_default_editor", "visTypeMarkdown": "src/plugins/vis_type_markdown", "visTypeMetric": "src/plugins/vis_type_metric", - "visTypeTable": "src/legacy/core_plugins/vis_type_table", - "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", + "visTypeTable": "src/plugins/vis_type_table", + "visTypeTagCloud": "src/plugins/vis_type_tagcloud", "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], - "visTypeVega": "src/legacy/core_plugins/vis_type_vega", + "visTypeVega": "src/plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", "visTypeXy": "src/legacy/core_plugins/vis_type_xy", "visualizations": "src/plugins/visualizations", diff --git a/.sass-lint.yml b/.sass-lint.yml index 5c2c88a1dad5d..89735342a2d6f 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -9,6 +9,7 @@ files: - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' - 'x-pack/plugins/lens/**/*.s+(a|c)ss' + - 'x-pack/plugins/cross_cluster_replication/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/maps/**/*.s+(a|c)ss' ignore: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md deleted file mode 100644 index 2ef8c797f4054..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) - -## AggConfigOptions.enabled property - -Signature: - -```typescript -enabled?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md deleted file mode 100644 index 8939854ab19ca..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) - -## AggConfigOptions.id property - -Signature: - -```typescript -id?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md index b841d9b04d6a7..ff8055b8cf1b1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md @@ -2,21 +2,12 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) -## AggConfigOptions interface +## AggConfigOptions type Signature: ```typescript -export interface AggConfigOptions +export declare type AggConfigOptions = Assign; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) | boolean | | -| [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) | string | | -| [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) | Record<string, any> | | -| [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) | string | | -| [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) | IAggType | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md deleted file mode 100644 index 45219a837cc33..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) - -## AggConfigOptions.params property - -Signature: - -```typescript -params?: Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md deleted file mode 100644 index b2b42eb2e5b4d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) - -## AggConfigOptions.schema property - -Signature: - -```typescript -schema?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md deleted file mode 100644 index 866065ce52ba6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) - -## AggConfigOptions.type property - -Signature: - -```typescript -type: IAggType; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md index 43f30d73ca6df..a91db7e7aac8b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md @@ -7,5 +7,5 @@ Signature: ```typescript -makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; +makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md index b75065da91abd..f9733529a315d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md @@ -21,5 +21,5 @@ export declare class AggParamType ex | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [allowedAggs](./kibana-plugin-plugins-data-public.aggparamtype.allowedaggs.md) | | string[] | | -| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: any) => TAggConfig | | +| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md index 50e8f2409ac02..ddbf1a8459d1f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md @@ -7,5 +7,5 @@ Signature: ```typescript -baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[] +baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[] ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md deleted file mode 100644 index 5c5aa348eecdf..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) - -## createSearchSource variable - -Deserializes a json string and a set of referenced objects to a `SearchSource` instance. Use this method to re-create the search source serialized using `searchSource.serialize`. - -This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service - -Signature: - -```typescript -createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md index 3e966caa30799..25ce6eaa688f8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md @@ -8,6 +8,7 @@ ```typescript actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md index a623e91388fd6..4f43f10ce089e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md @@ -14,7 +14,7 @@ export interface DataPublicPluginStart | Property | Type | Description | | --- | --- | --- | -| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromEvent: typeof createFiltersFromEvent;
} | | +| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction;
createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction;
} | | | [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | | | [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | | | [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md index 244633c3c4c9e..d39871b99f744 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md @@ -10,7 +10,7 @@ fieldFormats: { FieldFormat: typeof FieldFormat; FieldFormatsRegistry: typeof FieldFormatsRegistry; - serialize: (agg: import("./search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md index a8154dff72a6a..4b9f6e3594dc5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md @@ -4,6 +4,8 @@ ## ISearchSource type +\* + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index bf29c883e4eb9..604ac5120922b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -21,7 +21,6 @@ | [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | | -| [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | | | [TimeHistory](./kibana-plugin-plugins-data-public.timehistory.md) | | ## Enumerations @@ -50,7 +49,6 @@ | Interface | Description | | --- | --- | -| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | | | [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | | | [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | | @@ -101,7 +99,6 @@ | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | -| [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) | Deserializes a json string and a set of referenced objects to a SearchSource instance. Use this method to re-create the search source serialized using searchSource.serialize.This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | @@ -120,6 +117,7 @@ | Type Alias | Description | | --- | --- | +| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | @@ -140,7 +138,7 @@ | [IpRangeKey](./kibana-plugin-plugins-data-public.iprangekey.md) | | | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | -| [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | | +| [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | \* | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 78ac05b9fd386..9a22339fd0530 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -27,8 +27,9 @@ search: { InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; + isNumberType: (agg: import("./search").AggConfig) => boolean; isStringType: (agg: import("./search").AggConfig) => boolean; - isType: (type: string) => (agg: import("./search").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("./search").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parentPipelineType: string; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md deleted file mode 100644 index e0c9e77b313a5..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [(constructor)](./kibana-plugin-plugins-data-public.searchsource._constructor_.md) - -## SearchSource.(constructor) - -Constructs a new instance of the `SearchSource` class - -Signature: - -```typescript -constructor(fields?: SearchSourceFields); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| fields | SearchSourceFields | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md deleted file mode 100644 index b0a0201680ca8..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [create](./kibana-plugin-plugins-data-public.searchsource.create.md) - -## SearchSource.create() method - -Signature: - -```typescript -create(): SearchSource; -``` -Returns: - -`SearchSource` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md deleted file mode 100644 index 3f17dc21cf514..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [createChild](./kibana-plugin-plugins-data-public.searchsource.createchild.md) - -## SearchSource.createChild() method - -Signature: - -```typescript -createChild(options?: {}): SearchSource; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| options | {} | | - -Returns: - -`SearchSource` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md deleted file mode 100644 index f503a3dfc3299..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [createCopy](./kibana-plugin-plugins-data-public.searchsource.createcopy.md) - -## SearchSource.createCopy() method - -Signature: - -```typescript -createCopy(): SearchSource; -``` -Returns: - -`SearchSource` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md deleted file mode 100644 index 8a7cc5ee75d11..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [destroy](./kibana-plugin-plugins-data-public.searchsource.destroy.md) - -## SearchSource.destroy() method - -Completely destroy the SearchSource. {undefined} - -Signature: - -```typescript -destroy(): void; -``` -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md deleted file mode 100644 index 208ce565fac13..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [fetch](./kibana-plugin-plugins-data-public.searchsource.fetch.md) - -## SearchSource.fetch() method - -Fetch this source and reject the returned Promise on error - - -Signature: - -```typescript -fetch(options?: FetchOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| options | FetchOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md deleted file mode 100644 index 98ba815696cf6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getField](./kibana-plugin-plugins-data-public.searchsource.getfield.md) - -## SearchSource.getField() method - -Get fields from the fields - -Signature: - -```typescript -getField(field: K, recurse?: boolean): SearchSourceFields[K]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| field | K | | -| recurse | boolean | | - -Returns: - -`SearchSourceFields[K]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md deleted file mode 100644 index dce03e7e1a95c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md +++ /dev/null @@ -1,49 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getFields](./kibana-plugin-plugins-data-public.searchsource.getfields.md) - -## SearchSource.getFields() method - -Signature: - -```typescript -getFields(): { - type?: string | undefined; - query?: import("../..").Query | undefined; - filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record | Record[] | undefined; - highlight?: any; - highlightAll?: boolean | undefined; - aggs?: any; - from?: number | undefined; - size?: number | undefined; - source?: string | boolean | string[] | undefined; - version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; - index?: import("../..").IndexPattern | undefined; - searchAfter?: import("./types").EsQuerySearchAfter | undefined; - timeout?: string | undefined; - terminate_after?: number | undefined; - }; -``` -Returns: - -`{ - type?: string | undefined; - query?: import("../..").Query | undefined; - filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record | Record[] | undefined; - highlight?: any; - highlightAll?: boolean | undefined; - aggs?: any; - from?: number | undefined; - size?: number | undefined; - source?: string | boolean | string[] | undefined; - version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; - index?: import("../..").IndexPattern | undefined; - searchAfter?: import("./types").EsQuerySearchAfter | undefined; - timeout?: string | undefined; - terminate_after?: number | undefined; - }` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md deleted file mode 100644 index 55aaa26ca62f3..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getId](./kibana-plugin-plugins-data-public.searchsource.getid.md) - -## SearchSource.getId() method - -Signature: - -```typescript -getId(): string; -``` -Returns: - -`string` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md deleted file mode 100644 index d5a133772264e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getOwnField](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) - -## SearchSource.getOwnField() method - -Get the field from our own fields, don't traverse up the chain - -Signature: - -```typescript -getOwnField(field: K): SearchSourceFields[K]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| field | K | | - -Returns: - -`SearchSourceFields[K]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md deleted file mode 100644 index 14578f7949ba6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getParent](./kibana-plugin-plugins-data-public.searchsource.getparent.md) - -## SearchSource.getParent() method - -Get the parent of this SearchSource {undefined\|searchSource} - -Signature: - -```typescript -getParent(): SearchSource | undefined; -``` -Returns: - -`SearchSource | undefined` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md deleted file mode 100644 index f3451c9391074..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getSearchRequestBody](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) - -## SearchSource.getSearchRequestBody() method - -Signature: - -```typescript -getSearchRequestBody(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md deleted file mode 100644 index e77c9dac7239f..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [history](./kibana-plugin-plugins-data-public.searchsource.history.md) - -## SearchSource.history property - -Signature: - -```typescript -history: SearchRequest[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md deleted file mode 100644 index 5f2fc809a5590..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ /dev/null @@ -1,46 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) - -## SearchSource class - -Signature: - -```typescript -export declare class SearchSource -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(fields)](./kibana-plugin-plugins-data-public.searchsource._constructor_.md) | | Constructs a new instance of the SearchSource class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [history](./kibana-plugin-plugins-data-public.searchsource.history.md) | | SearchRequest[] | | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [create()](./kibana-plugin-plugins-data-public.searchsource.create.md) | | | -| [createChild(options)](./kibana-plugin-plugins-data-public.searchsource.createchild.md) | | | -| [createCopy()](./kibana-plugin-plugins-data-public.searchsource.createcopy.md) | | | -| [destroy()](./kibana-plugin-plugins-data-public.searchsource.destroy.md) | | Completely destroy the SearchSource. {undefined} | -| [fetch(options)](./kibana-plugin-plugins-data-public.searchsource.fetch.md) | | Fetch this source and reject the returned Promise on error | -| [getField(field, recurse)](./kibana-plugin-plugins-data-public.searchsource.getfield.md) | | Get fields from the fields | -| [getFields()](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | | -| [getId()](./kibana-plugin-plugins-data-public.searchsource.getid.md) | | | -| [getOwnField(field)](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) | | Get the field from our own fields, don't traverse up the chain | -| [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | -| [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | | -| [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start | -| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index.Using createSearchSource, the instance can be re-created. | -| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | | -| [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | | -| [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from | -| [setPreferredSearchStrategyId(searchStrategyId)](./kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md) | | \*\*\* PUBLIC API \*\*\* | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md deleted file mode 100644 index 092d057c69196..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [onRequestStart](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) - -## SearchSource.onRequestStart() method - -Add a handler that will be notified whenever requests start - -Signature: - -```typescript -onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| handler | (searchSource: ISearchSource, options?: FetchOptions) => Promise<unknown> | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md deleted file mode 100644 index 52d25dec01dfd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [serialize](./kibana-plugin-plugins-data-public.searchsource.serialize.md) - -## SearchSource.serialize() method - -Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object. - -The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. - -Using `createSearchSource`, the instance can be re-created. - -Signature: - -```typescript -serialize(): { - searchSourceJSON: string; - references: SavedObjectReference[]; - }; -``` -Returns: - -`{ - searchSourceJSON: string; - references: SavedObjectReference[]; - }` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md deleted file mode 100644 index 83b7c30281752..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setField](./kibana-plugin-plugins-data-public.searchsource.setfield.md) - -## SearchSource.setField() method - -Signature: - -```typescript -setField(field: K, value: SearchSourceFields[K]): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| field | K | | -| value | SearchSourceFields[K] | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md deleted file mode 100644 index fa9b265aa43b7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setFields](./kibana-plugin-plugins-data-public.searchsource.setfields.md) - -## SearchSource.setFields() method - -Signature: - -```typescript -setFields(newFields: SearchSourceFields): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| newFields | SearchSourceFields | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md deleted file mode 100644 index 19bf10bec210f..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setParent](./kibana-plugin-plugins-data-public.searchsource.setparent.md) - -## SearchSource.setParent() method - -Set a searchSource that this source should inherit from - -Signature: - -```typescript -setParent(parent?: ISearchSource, options?: SearchSourceOptions): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| parent | ISearchSource | | -| options | SearchSourceOptions | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md deleted file mode 100644 index 8d8dbce9e60f6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setPreferredSearchStrategyId](./kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md) - -## SearchSource.setPreferredSearchStrategyId() method - -\*\*\* PUBLIC API \*\*\* - -Signature: - -```typescript -setPreferredSearchStrategyId(searchStrategyId: string): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| searchStrategyId | string | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md index 2b986aee508e2..11f18a195d271 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md @@ -10,7 +10,7 @@ fieldFormats: { FieldFormatsRegistry: typeof FieldFormatsRegistry; FieldFormat: typeof FieldFormat; - serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index 5c2f542204079..4d7a0b3cfbbca 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -9,7 +9,7 @@ ```typescript setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; @@ -26,7 +26,7 @@ setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { `{ fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }` diff --git a/docs/images/clone_panel.gif b/docs/images/clone_panel.gif new file mode 100644 index 0000000000000..e521e438d051a Binary files /dev/null and b/docs/images/clone_panel.gif differ diff --git a/docs/images/report-automate-csv.png b/docs/images/report-automate-csv.png new file mode 100644 index 0000000000000..fba77821ae29f Binary files /dev/null and b/docs/images/report-automate-csv.png differ diff --git a/docs/images/report-automate-pdf.png b/docs/images/report-automate-pdf.png new file mode 100644 index 0000000000000..f96eebe6fe02d Binary files /dev/null and b/docs/images/report-automate-pdf.png differ diff --git a/docs/images/vega_lite_default.png b/docs/images/vega_lite_default.png new file mode 100644 index 0000000000000..1ce1592d0738c Binary files /dev/null and b/docs/images/vega_lite_default.png differ diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index f62a4d28dfc0d..7081590931a99 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -217,6 +217,8 @@ might increase the search time. This setting is off by default. Users must opt-i [horizontal] `siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. `siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. +`siem:ipReputationLinks`:: A JSON array containing links for verifying the reputation of an IP address. The links are displayed on +{siem-guide}/siem-ui-overview.html#network-ui[IP detail] pages. `siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* page. `siem:newsFeedUrl`:: The URL from which the security news feed content is diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 6528568e86890..b71e1c672756a 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -8,7 +8,6 @@ You do not need to configure any settings to use {kib} {ml-features}. They are enabled by default. -[float] [[general-ml-settings-kb]] ==== General {ml} settings @@ -19,3 +18,11 @@ If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, you can still use the {ml} APIs. To disable {ml} entirely, see the {ref}/ml-settings.html[{es} {ml} settings]. + +[[data-visualizer-settings]] +==== {data-viz} settings + +`xpack.ml.file_data_visualizer.max_file_size`:: +Sets the file size limit when importing data in the {data-viz}. The default +value is `100MB`. The highest supported value for this setting is `1GB`. + diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index ddabce3d5b842..12ee96b21b0c7 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -94,7 +94,7 @@ Some example translations are shown here: **Environment Variable**:: **Kibana Setting** `SERVER_NAME`:: `server.name` `KIBANA_DEFAULTAPPID`:: `kibana.defaultAppId` -`XPACK_MONITORING_ENABLED`:: `xpack.monitoring.enabled` +`MONITORING_ENABLED`:: `monitoring.enabled` In general, any setting listed in <> can be configured with this technique. @@ -125,9 +125,9 @@ images: `server.name`:: `kibana` `server.host`:: `"0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` -`xpack.monitoring.ui.container.elasticsearch.enabled`:: `true` +`monitoring.ui.container.elasticsearch.enabled`:: `true` -NOTE: The setting `xpack.monitoring.ui.container.elasticsearch.enabled` is not +NOTE: The setting `monitoring.ui.container.elasticsearch.enabled` is not defined in the `-oss` image. These settings are defined in the default `kibana.yml`. They can be overridden diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc index eef2b11e53d85..19f9d64d13623 100644 --- a/docs/setup/production.asciidoc +++ b/docs/setup/production.asciidoc @@ -133,7 +133,8 @@ server.port Settings that must be the same: -------- xpack.security.encryptionKey //decrypting session cookies -xpack.reporting.encryptionKey //decrypting reports stored in Elasticsearch +xpack.reporting.encryptionKey //decrypting reports +xpack.encryptedSavedObjects.encryptionKey // decrypting saved objects -------- Separate configuration files can be used from the command line by using the `-c` flag: diff --git a/docs/siem/images/cases-ui.png b/docs/siem/images/cases-ui.png new file mode 100644 index 0000000000000..b513efb664740 Binary files /dev/null and b/docs/siem/images/cases-ui.png differ diff --git a/docs/siem/siem-ui.asciidoc b/docs/siem/siem-ui.asciidoc index 85253daaf2933..985138756622d 100644 --- a/docs/siem/siem-ui.asciidoc +++ b/docs/siem/siem-ui.asciidoc @@ -35,7 +35,7 @@ image::siem/images/network-ui.png[] [float] [[detections-ui]] -=== Detections (Beta) +=== Detections (beta) The Detections feature automatically searches for threats and creates signals when they are detected. Signal detection rules define the conditions @@ -50,6 +50,22 @@ or the Detections API. [role="screenshot"] image::siem/images/detections-ui.png[] +[float] +[[cases-ui]] +=== Cases (beta) + +Cases are used to open and track security issues directly in SIEM. +Cases list the original reporter and all users who contribute to a case +(`participants`). Case comments support Markdown syntax, and allow linking to +saved Timelines. Additionally, you can send cases to external systems from +within SIEM (currently ServiceNow). + +For information about opening, updating, and closing cases, see +{siem-guide}/cases-overview.html[Cases] in the SIEM Guide. + +[role="screenshot"] +image::siem/images/cases-ui.png[] + [float] [[timelines-ui]] === Timeline diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index be3623dd9e59c..794fc14005f2f 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[email-action-type]] -== Email action type +=== Email action The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. @@ -10,11 +10,11 @@ The email action type uses the SMTP protocol to send mail message, using an inte Email connectors have the following configuration properties: -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. -Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. +Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. Username:: username for 'login' type authentication. Password:: password for 'login' type authentication. @@ -26,4 +26,4 @@ Email actions have the following configuration properties: To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. Subject:: The subject line of the email. -Message:: The message text of the email. Markdown format is supported. \ No newline at end of file +Message:: The message text of the email. Markdown format is supported. diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc index 75d9e57b1f212..625b8f704b7c6 100644 --- a/docs/user/alerting/action-types/index.asciidoc +++ b/docs/user/alerting/action-types/index.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[index-action-type]] -== Index action type +=== Index action The index action type will index a document into {es}. @@ -21,4 +21,4 @@ Execution time field:: This field will be automatically set to the time the ale Index actions have the following properties: -Document:: The document to index in json format. \ No newline at end of file +Document:: The document to index in json format. diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index da34c6e0855d7..abdcc7d1ba524 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[pagerduty-action-type]] -== PagerDuty action type +=== PagerDuty action The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. @@ -10,7 +10,7 @@ The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/event [float] [[pagerduty-benefits]] -=== PagerDuty + Elastic integration benefits +==== PagerDuty + Elastic integration benefits By integrating PagerDuty with alerts, you can: @@ -20,7 +20,7 @@ By integrating PagerDuty with alerts, you can: [float] [[pagerduty-how-it-works]] -==== How it works +===== How it works {kib} allows you to create alerts to notify you of a significant move in your dataset. @@ -28,7 +28,7 @@ You can create alerts for all your Observability, Security, and Elastic Stack us Alerts will trigger a new incident on the corresponding PagerDuty service. [float] -==== Requirements +===== Requirements In the `kibana.yml` configuration file, you must add the <>. This is required to encrypt parameters that must be secured, for example PagerDuty’s integration key. @@ -47,18 +47,17 @@ review the <> that are available to you. [float] [[pagerduty-support]] -==== Support +===== Support If you need help with this integration, get in touch with the {kib} team by visiting https://support.elastic.co[support.elastic.co] or by using the *Ask Elastic* option in the {kib} Help menu. You can also select the {kib} category at https://discuss.elastic.co/[discuss.elastic.co]. [float] [[pagerduty-integration-walkthrough]] -==== Integration with PagerDuty walkthrough +===== Integration with PagerDuty walkthrough -[float] [[pagerduty-in-pagerduty]] -===== In PagerDuty +*In PagerDuty* . From the *Configuration* menu, select *Services*. . Add an integration to a service: @@ -83,9 +82,8 @@ image::user/alerting/images/pagerduty-integration.png[PagerDuty Integrations tab . Save this key, as you will use it when you configure the integration with Elastic in the next section. -[float] [[pagerduty-in-elastic]] -===== In Elastic +*In Elastic* . Create a PagerDuty Connector in Kibana. You can: + @@ -117,7 +115,7 @@ https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[API v2 do [float] [[pagerduty-uninstall]] -==== How to uninstall +===== How to uninstall To remove a PagerDuty connector from an alert, simply remove it from the *Actions* section of that alert, using the remove (x) icon. This will disable the integration for the particular alert. @@ -129,7 +127,7 @@ This is an irreversible action and impacts all alerts that use this connector. [float] [[pagerduty-connector-configuration]] -=== Connector configuration +==== Connector configuration PagerDuty connectors have the following configuration properties: @@ -139,7 +137,7 @@ Routing Key:: A 32 character PagerDuty Integration Key for an integration on a [float] [[pagerduty-action-configuration]] -=== Action configuration +==== Action configuration PagerDuty actions have the following properties: diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc index 4efbdf3bea099..8f888785626c9 100644 --- a/docs/user/alerting/action-types/server-log.asciidoc +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[server-log-action-type]] -== Server log action type +=== Server log action This action type writes and entry to the {kib} server log. @@ -18,4 +18,4 @@ Name:: The name of the connector. The name is used to identify a connector Server log actions have the following properties: -Message:: The message to log. \ No newline at end of file +Message:: The message to log. diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc index a4bacbf162e46..c0965d65bfdbe 100644 --- a/docs/user/alerting/action-types/slack.asciidoc +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[slack-action-type]] -== Slack action type +=== Slack action -The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. +The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. [float] [[slack-connector-configuration]] @@ -11,7 +11,7 @@ The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incomin Slack connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. +Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. [float] [[slack-action-configuration]] @@ -19,4 +19,4 @@ Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messa Slack actions have the following properties: -Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. \ No newline at end of file +Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index 8c211aa83af89..64bfa6a1d6364 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[webhook-action-type]] -== Webhook action type +=== Webhook action The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. @@ -11,7 +11,7 @@ The Webhook action type uses https://github.com/axios/axios[axios] to send a POS Webhook connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. +URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. Method:: HTTP request method, either `post`(default) or `put`. Headers:: A set of key-value pairs sent as headers with the request User:: An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported. @@ -23,4 +23,4 @@ Password:: An optional password. If set, HTTP basic authentication is used. Cur Webhook actions have the following properties: -Body:: A json payload sent to the request URL. \ No newline at end of file +Body:: A json payload sent to the request URL. diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc index 3db13acfb423e..4c408da92f579 100644 --- a/docs/user/alerting/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/pre-configured-connectors.asciidoc @@ -20,8 +20,7 @@ action are predefined, including the connector name and ID. The following example shows a valid configuration 2 out-of-the box connector. -[source,console] ------------------------- +```js xpack.actions.preconfigured: - id: 'my-slack1' <1> actionTypeId: .slack <2> @@ -40,7 +39,7 @@ The following example shows a valid configuration 2 out-of-the box connector. secrets: <5> user: elastic password: changeme ------------------------- +``` <1> `id` is the action connector identifier. <2> `actionTypeId` is the action type identifier. diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index ab529a533d5e3..de714ae40086b 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -98,6 +98,24 @@ to the new dimensions. * To delete a panel, open the panel menu and select *Delete from dashboard.* Deleting a panel from a dashboard does *not* delete the saved visualization or search. +[float] +[[cloning-a-panel]] +=== Clone dashboard elements + +In *Edit* mode, you can clone any panel on a dashboard. + +To clone an existing panel, open the panel menu of the element you wish to clone, then select *Clone panel*. + +* Cloned panels appear beside the original, and will move other panels down to make room if necessary. + +* Clones support all of the original panel's functionality, including renaming, editing, and cloning. + +* All cloned visualizations will appear in the visualization list. + +[role="screenshot"] +image:images/clone_panel.gif[clone panel] + + [float] [[viewing-detailed-information]] === Inspect and edit elements diff --git a/docs/user/ml/images/ml-data-visualizer-sample.jpg b/docs/user/ml/images/ml-data-visualizer-sample.jpg index 6c2e018932717..ce2bb660d7da1 100644 Binary files a/docs/user/ml/images/ml-data-visualizer-sample.jpg and b/docs/user/ml/images/ml-data-visualizer-sample.jpg differ diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index cca0dc5e4530f..e9ef4a55b2b3a 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -4,31 +4,31 @@ [partintro] -- -As datasets increase in size and complexity, the human effort required to +As data sets increase in size and complexity, the human effort required to inspect dashboards or maintain rules for spotting infrastructure problems, cyber attacks, or business issues becomes impractical. Elastic {ml-features} such as {anomaly-detect} and {oldetection} make it easier to notice suspicious activities with minimal human interference. -If you have a basic license, you can use the *Data Visualizer* to learn more -about your data. In particular, if your data is stored in {es} and contains a -time field, you can use the *Data Visualizer* to identify possible fields for -{anomaly-detect}: +{kib} includes a free *{data-viz}* to learn more about your data. In particular, +if your data is stored in {es} and contains a time field, you can use the +*{data-viz}* to identify possible fields for {anomaly-detect}: [role="screenshot"] -image::user/ml/images/ml-data-visualizer-sample.jpg[Data Visualizer for sample flight data] +image::user/ml/images/ml-data-visualizer-sample.jpg[{data-viz} for sample flight data] -experimental[] You can also upload a CSV, NDJSON, or log file (up to 100 MB in -size). The *Data Visualizer* identifies the file format and field mappings. You -can then optionally import that data into an {es} index. +experimental[] You can also upload a CSV, NDJSON, or log file. The *{data-viz}* +identifies the file format and field mappings. You can then optionally import +that data into an {es} index. To change the default file size limit, see +<>. -You need the following permissions to use the Data Visualizer with file upload: +You need the following permissions to use the {data-viz} with file upload: * cluster privileges: `monitor`, `manage_ingest_pipelines` * index privileges: `read`, `manage`, `index` For more information, see {ref}/security-privileges.html[Security privileges] -and {ref}/built-in-roles.html[Built-in roles]. +and {ml-docs}/setup.html[Set up {ml-features}]. -- diff --git a/docs/user/monitoring/cluster-alerts.asciidoc b/docs/user/monitoring/cluster-alerts.asciidoc index cfdc9e2037030..a58ccc7f7d68d 100644 --- a/docs/user/monitoring/cluster-alerts.asciidoc +++ b/docs/user/monitoring/cluster-alerts.asciidoc @@ -49,13 +49,13 @@ To receive email notifications for the Cluster Alerts: . Configure an email account as described in {ref}/actions-email.html#configuring-email[Configuring email accounts]. . Configure the -`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +`monitoring.cluster_alerts.email_notifications.email_address` setting in `kibana.yml` with your email address. + -- TIP: If you have separate production and monitoring clusters and separate {kib} instances for those clusters, you must put the -`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +`monitoring.cluster_alerts.email_notifications.email_address` setting in the {kib} instance that is associated with the production cluster. -- diff --git a/docs/user/monitoring/elasticsearch-details.asciidoc b/docs/user/monitoring/elasticsearch-details.asciidoc index c0e804672d298..93f809cfff650 100644 --- a/docs/user/monitoring/elasticsearch-details.asciidoc +++ b/docs/user/monitoring/elasticsearch-details.asciidoc @@ -164,4 +164,4 @@ image::user/monitoring/images/monitoring-elasticsearch-logs.jpg["Recent {es} log TIP: By default, up to 10 log entries are shown. You can show up to 50 log entries by changing the -<>. +<>. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index d0f2bd6acd901..9aa10289d299b 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -61,8 +61,8 @@ For more information, see {ref}/monitoring-settings.html[Monitoring settings in and {ref}/cluster-update-settings.html[Cluster update settings]. -- -. Verify that `xpack.monitoring.enabled` and -`xpack.monitoring.kibana.collection.enabled` are set to `true` in the +. Verify that `monitoring.enabled` and +`monitoring.kibana.collection.enabled` are set to `true` in the `kibana.yml` file. These are the default values. For more information, see <>. diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index f03a2ce1525a4..61aeaf21d3a4b 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -25,10 +25,10 @@ Add the following setting in the {kib} configuration file (`kibana.yml`): [source,yaml] ---------------------------------- -xpack.monitoring.kibana.collection.enabled: false +monitoring.kibana.collection.enabled: false ---------------------------------- -Leave the `xpack.monitoring.enabled` set to its default value (`true`). +Leave the `monitoring.enabled` set to its default value (`true`). // end::disable-kibana-collection[] For more information, see <>. diff --git a/docs/user/monitoring/monitoring-troubleshooting.asciidoc b/docs/user/monitoring/monitoring-troubleshooting.asciidoc index 7e1d6f94f15fa..bdaa10990c3aa 100644 --- a/docs/user/monitoring/monitoring-troubleshooting.asciidoc +++ b/docs/user/monitoring/monitoring-troubleshooting.asciidoc @@ -52,7 +52,7 @@ The *Stack Monitoring* page in {kib} is empty. . Confirm that {kib} is seeking monitoring data from the appropriate {es} URL. By default, data is retrieved from the cluster specified in the `elasticsearch.hosts` setting in the `kibana.yml` file. If you want to retrieve it -from a different monitoring cluster, set `xpack.monitoring.elasticsearch.hosts`. +from a different monitoring cluster, set `monitoring.ui.elasticsearch.hosts`. See <>. . Confirm that there is monitoring data available at that URL. It is stored in diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 11516e32400fb..0a5535e6e1a91 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -26,14 +26,14 @@ cluster and view them all through the same instance of {kib}. By default, data is retrieved from the cluster specified in the `elasticsearch.hosts` value in the `kibana.yml` file. If you want to retrieve it -from a different cluster, set `xpack.monitoring.elasticsearch.hosts`. +from a different cluster, set `monitoring.ui.elasticsearch.hosts`. To learn more about typical monitoring architectures, see {ref}/how-monitoring-works.html[How monitoring works] and {ref}/monitoring-production.html[Monitoring in a production environment]. -- -. Verify that `xpack.monitoring.ui.enabled` is set to `true`, which is the +. Verify that `monitoring.ui.enabled` is set to `true`, which is the default value, in the `kibana.yml` file. For more information, see <>. @@ -43,8 +43,8 @@ must provide a user ID and password so {kib} can retrieve the data. .. Create a user that has the `monitoring_user` {ref}/built-in-roles.html[built-in role] on the monitoring cluster. -.. Add the `xpack.monitoring.elasticsearch.username` and -`xpack.monitoring.elasticsearch.password` settings in the `kibana.yml` file. +.. Add the `monitoring.ui.elasticsearch.username` and +`monitoring.ui.elasticsearch.password` settings in the `kibana.yml` file. If these settings are omitted, {kib} uses the `elasticsearch.username` and `elasticsearch.password` setting values. For more information, see {kibana-ref}/using-kibana-with-security.html[Configuring security in {kib}]. diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 5d35f103ecee0..3e227229ddcc5 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -1,32 +1,42 @@ [role="xpack"] [[automating-report-generation]] == Automating report generation -You can automatically generate reports with {watcher}, or by submitting -HTTP `POST` requests from a script. +Automatically generate PDF and CSV reports by submitting HTTP `POST` requests using {watcher} or a script. include::report-intervals.asciidoc[] [float] -=== Get the POST URL +=== Create a POST URL -Generating a report either through {watcher} or a script requires capturing the **POST -URL**, which is the URL to queue a report for generation. +Create the POST +URL that triggers a report to generate. -To get the URL for triggering PDF report generation during a given time period: +To create the POST URL for PDF reports: -. Load the saved object in *Visualize* or *Dashboard*. -. To specify a relative or absolute time period, use the time filter. -. In the {kib} toolbar, click *Share*. -. Select *PDF Reports*. -. Click **Copy POST URL**. +. Go to *Visualize* or *Dashboard*, then open the visualization or dashboard. ++ +To specify a relative or absolute time period, use the time filter. -To get the URL for triggering CSV report generation during a given time period: +. From the {kib} toolbar, click *Share*, then select *PDF Reports*. + +. Click *Copy POST URL*. ++ +[role="screenshot"] +image::images/report-automate-pdf.png[Generate Visualize and Dashboard reports] + + +To create the POST URL for CSV reports: . Load the saved search in *Discover*. -. To specify a relative or absolute time period, use the time filter. -. In the {kib} toolbar, click *Share*. -. Select *CSV Reports*. -. Click **Copy POST URL**. ++ +To specify a relative or absolute time period, use the time filter. + +. From the {kib} toolbar, click *Share*, then select *CSV Reports*. + +. Click *Copy POST URL*. ++ +[role="screenshot"] +image::images/report-automate-csv.png[Generate Discover reports] [float] === Use Watcher diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc index 97313c19f44cb..3bdc59b90b3fd 100644 --- a/docs/user/security/securing-communications/index.asciidoc +++ b/docs/user/security/securing-communications/index.asciidoc @@ -188,5 +188,5 @@ verification. For more information about this setting, see <>. + +* *Pin filters to global state* — When selected, all filters created by interacting with the inputs are automatically pinned. + +. Click *Update*. + [float] [[markdown-widget]] === Markdown diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index b181763c0d0d0..422afbb201183 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -14,20 +14,6 @@ beta[] * Save your visualization for use in a dashboard. -[float] -[[lens-aggregation]] -=== Supported aggregations - -Lens supports the following aggregations: - -* <> - -* <> - -* <> - -* <> - [float] [[drag-drop]] === Drag and drop diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc index ba291e3cc6859..f716930e7e65c 100644 --- a/docs/visualize/most-frequent.asciidoc +++ b/docs/visualize/most-frequent.asciidoc @@ -13,20 +13,6 @@ The most frequently used visualizations include: [[metric-chart]] -[float] -[[frequently-used-viz-aggregation]] -=== Supported aggregations - -The most frequently used visualizations support the following aggregations: - -* <> - -* <> - -* <> - -* <> - [float] === Configure your visualization diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 51342847080e0..c889bd0bb6ca0 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -6,19 +6,6 @@ Display graphical representations of data where the individual values are repres [role="screenshot"] image::images/visualize_heat_map_example.png[] -[float] -[[build-heat-map]] -=== Build a heat map - -To display your data on the heat map, use the supported aggregations. - -Heat maps support the following aggregations: - -* <> -* <> -* <> -* <> - [float] [[navigate-heatmap]] === Change the color ranges diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc index 69d6985acd1e4..36709c2cc6437 100644 --- a/docs/visualize/tsvb.asciidoc +++ b/docs/visualize/tsvb.asciidoc @@ -43,18 +43,6 @@ Table:: Display data from multiple time series by defining the field group to sh [role="screenshot"] image:images/tsvb-table.png["Table visualization"] -[float] -[[tsvb-aggregation]] -=== Supported aggregations - -TSVB supports the following aggregations: - -* <> - -* <> - -* <> - [float] [[create-tsvb-visualization]] === Create TSVB visualizations diff --git a/docs/visualize/vega.asciidoc b/docs/visualize/vega.asciidoc index b8c0d1dbe3dda..e0d9955f0c3db 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/visualize/vega.asciidoc @@ -1,81 +1,80 @@ [[vega-graph]] -== Vega Graphs -experimental[] +== Vega -You can build https://vega.github.io/vega/examples/[Vega] and -https://vega.github.io/vega-lite/examples/[Vega-Lite] data visualizations -into Kibana, either standalone, or on top of a map. To see Vega in action, -watch this -https://www.youtube.com/watch?v=lQGCipY3th8[short introduction video]. +experimental[] -Currently Vega version 4.3 and Vega-Lite version 2.6 are supported. +Build custom visualizations from multiple data sources using Vega +and Vega-Lite. -NOTE: In Vega it is possible to load data dynamically, e.g. by setting signals as data urls. This is not supported in Kibana as all data is fetched at once prior to passing it to the Vega renderer. +* *Vega* — A declarative format to create visualizations using JSON. + Generate interactive displays using D3. +* *Vega-Lite* — An easier format to use than Vega that enables more rapid + data analysis. Compiles into Vega. -[[vega-quick-demo]] -=== Getting Started with Vega +For more information about Vega and Vega-Lite, refer to +<>. -* To experiment using sample data, first click the {kib} logo in the upper left hand corner -and then click the link next to *Sample Data*. -* Once you have data loaded, go to *Visualize*, click *+*, and select *Vega* to see an example graph. -*Note*: The default graph is written in Vega-Lite, but you can build visualizations -in either language. See <> for more information. -* Try changing `mark` from `line` to `point`, `area`, `bar`, `circle`, -or `square`. Check out the -https://vega.github.io/vega-lite/docs/mark.html#mark-def[Vega-Lite docs] for more information. -* Explore other available https://vega.github.io/vega/examples/[Vega] or -https://vega.github.io/vega-lite/examples/[Vega-Lite] visualizations. -*Note*: You might need to make URLs absolute, for example, replace -`"url": "data/world-110m.json"` with -`"url": "https://vega.github.io/editor/data/world-110m.json"`. -See <>. -* For more information on getting started, check out this https://www.elastic.co/blog/getting-started-with-vega-visualizations-in-kibana[blog post]. +[float] +[[create-vega-viz]] +=== Create Vega visualizations +You create Vega visualizations by using the text editor, which is +preconfigured with the options you need. -[[vega-vs-vegalite]] -=== Vega vs Vega-Lite +[role="screenshot"] +image::images/vega_lite_default.png[] -The Vega visualization in {kib} supports both Vega and Vega-Lite. You can use the -`schema` value to define which language you would like to use and its minimum -required version. +[float] +[[vega-schema]] +==== Change the Vega version -For example: +The default visualization uses Vega-Lite version 2. To use Vega version 4, edit +the `schema`. -* Vega-Lite v2: `$schema: https://vega.github.io/schema/vega-lite/v2.json` -* Vega v4: `$schema: https://vega.github.io/schema/vega/v4.json` +Go to `$schema`, enter `https://vega.github.io/schema/vega/v4.json`, then click +*Update*. -The `schema` URL is only used for identification, and does not need to be accessible by {kib}. +[float] +[[vega-type]] +==== Change the visualization type -Vega-Lite is a simplified version of Vega; it automates some constructions and has -much shorter specifications than Vega. Vega-Lite is automatically converted into -Vega before rendering, but it has some limitations, and there are some visualizations -that can be expressed in Vega that cannot be expressed in Vega-Lite. You can learn more -in the https://vega.github.io/vega-lite/[Vega-Lite documentation]. +The default visualization is a line chart. To change the visualization type, +change the `mark` value. The supported visualization types are listed in the +text editor. -You can use https://vega.github.io/editor/[this editor] to convert Vega-Lite into -Vega. +Go to `mark`, change the value to a different visualization type, then click +*Update*. -When you create a Vega visualization in {kib}, you can edit the `schema` -value in the dev tools to the left of the graph to define which of the two expression -languages you would like to use. +[float] +[[vega-sizing-and-positioning]] +==== Change the layout +By default, Vega visualizations use the `autosize = { type: 'fit', contains: 'padding' }` layout. +`fit` uses all available space, ignores `width` and `height` values, +and respects the padding values. To override this behavior, change the +`autosize` value. [[vega-querying-elasticsearch]] -=== Querying Elasticsearch +=== Query {es} -By default, Vega's https://vega.github.io/vega/docs/data/[data] element -can use embedded and external data with a `"url"` parameter. Kibana adds support for the direct Elasticsearch queries by overloading +experimental[] Vega https://vega.github.io/vega/docs/data/[data] elements +use embedded and external data with a `"url"` parameter. {kib} adds support for +direct {es} queries by overloading the `"url"` value. -Here is an example of an Elasticsearch query that counts the number of documents in all indexes. The query uses *@timestamp* field to filter the time range, and break it into histogram buckets. +NOTE: With Vega, you dynamically load your data by setting signals as data URLs. +Since {kib} is unable to support dynamically loaded data, all data is fetched +before it's passed to the Vega renderer. + +For example, count the number of documents in all indices: [source,yaml] ---- -// An object instead of a string for the url value +// An object instead of a string for the URL value // is treated as a context-aware Elasticsearch query. url: { - // Specify the time filter (upper right corner) with this field + // Specify the time filter. %timefield%: @timestamp // Apply dashboard context filters when set %context%: true @@ -88,8 +87,8 @@ url: { time_buckets: { date_histogram: { // Use date histogram aggregation on @timestamp field - field: @timestamp - // interval value will depend on the daterange picker + field: @timestamp <1> + // interval value will depend on the time filter // Use an integer to set approximate bucket count interval: { %autointerval%: true } // Make sure we get an entire range, even if it has no data @@ -109,7 +108,10 @@ url: { } ---- -The full result has this kind of structure: +<1> `@timestamp` — Filters the time range and breaks it into histogram +buckets. + +The full result includes the following structure: [source,yaml] ---- @@ -118,23 +120,24 @@ The full result has this kind of structure: "time_buckets": { "buckets": [{ "key_as_string": "2015-11-30T22:00:00.000Z", - "key": 1448920800000, + "key": 1448920800000,<1> "doc_count": 28 }, { "key_as_string": "2015-11-30T23:00:00.000Z", - "key": 1448924400000, + "key": 1448924400000, <1> "doc_count": 330 }, ... ---- -Note that `"key"` is a unix timestamp, and can be used without conversions by the +<1> `"key"` — The unix timestamp you can use without conversions by the Vega date expressions. -For most graphs we only need the list of the bucket values, so we use `format: {property: "aggregations.time_buckets.buckets"}` expression to focus on just the data we need. +For most visualizations, you only need the list of bucket values. To focus on +only the data you need, use `format: {property: "aggregations.time_buckets.buckets"}`. -Query may be specified with individual range and dashboard context as -well. This query is equivalent to `"%context%": true, "%timefield%": "@timestamp"`, -except that the timerange is shifted back by 10 minutes: +Specify a query with individual range and dashboard context. The query is +equivalent to `"%context%": true, "%timefield%": "@timestamp"`, +except that the time range is shifted back by 10 minutes: [source,yaml] ---- @@ -185,9 +188,9 @@ on the currently picked range: `"interval": {"%autointerval%": 10}` will try to get about 10-15 data points (buckets). [[vega-esmfiles]] -=== Elastic Map Files +=== Access Elastic Map Service files -It is possible to access Elastic Map Service's files via the same mechanism +experimental[] Access the Elastic Map Service files via the same mechanism: [source,yaml] ---- @@ -203,11 +206,8 @@ url: { format: {property: "features"} ---- -[[vega-with-a-map]] -=== Vega with a Map - -Kibana's default map can be used as a base of the Vega graph. To enable, -the graph must specify `type=map` in the host configuration: +To enable Elastic Maps, the graph must specify `type=map` in the host +configuration: [source,yaml] ---- @@ -247,42 +247,47 @@ the graph must specify `type=map` in the host configuration: } ---- -This visualization will automatically inject a projection called -`"projection"`. Use it to calculate positioning of all geo-aware marks. -Additionally, you may use `latitude`, `longitude`, and `zoom` signals. +The visualization automatically injects a `"projection"`, which you can use to +calculate the position of all geo-aware marks. +Additionally, you can use `latitude`, `longitude`, and `zoom` signals. These signals can be used in the graph, or can be updated to modify the -positioning of the map. +position of the map. + +Vega visualization ignore the `autosize`, `width`, `height`, and `padding` +values, using `fit` model with zero padding. [[vega-debugging]] -=== Debugging +=== Debugging Vega [[vega-browser-debugging-console]] -==== Browser Debugging console +==== Browser debugging console -Use browser debugging tools (e.g. F12 or Ctrl+Shift+J in Chrome) to +experimental[] Use browser debugging tools (for example, F12 or Ctrl+Shift+J in Chrome) to inspect the `VEGA_DEBUG` variable: -* `view` - access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] - on how to inspect data and signals at runtime. For Vega-Lite, `VEGA_DEBUG.view.data('source_0')` gets the main data set. - For Vega, it uses the data name as defined in your Vega spec. -* `vega_spec` - Vega JSON graph specification after some modifications by Kibana. In case ++ +* `view` — Access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] +on how to inspect data and signals at runtime. For Vega-Lite, `VEGA_DEBUG.view.data('source_0')` gets the main data set. +For Vega, it uses the data name as defined in your Vega spec. + +* `vega_spec` — Vega JSON graph specification after some modifications by {kib}. In case of Vega-Lite, this is the output of the Vega-Lite compiler. -* `vegalite_spec` - If this is a Vega-Lite graph, JSON specification of the graph before + +* `vegalite_spec` — If this is a Vega-Lite graph, JSON specification of the graph before Vega-Lite compilation. [[vega-data]] ==== Data -If you are using Elasticsearch query, make sure your resulting data is -what you expected. The easiest way to view it is by using "networking" -tab in the browser debugging tools (e.g. F12). Modify the graph slightly +experimental[] If you are using an {es} query, make sure your resulting data is +what you expected. The easiest way to view it is by using the "networking" +tab in the browser debugging tools (for example, F12). Modify the graph slightly so that it makes a search request, and view the response from the server. Another approach is to use -https://www.elastic.co/guide/en/kibana/current/console-kibana.html[Kibana -Dev Tools] tab - place the index name into the first line: -`GET /_search`, and add your query as the following lines -(just the value of the `"query"` field) +https://www.elastic.co/guide/en/kibana/current/console-kibana.html[Dev Tools]. Place the index name into the first line: +`GET /_search`, then add your query as the following lines +(just the value of the `"query"` field). -If you need to share your graph with someone, you may want to copy the +If you need to share your graph with someone, copy the raw data response to https://gist.github.com/[gist.github.com], possibly with a `.json` extension, use the `[raw]` button, and use that url directly in your graph. @@ -292,9 +297,11 @@ to your kibana.yml file. [[vega-notes]] [[vega-useful-links]] -=== Useful Links +=== Resources and examples + +experimental[] To learn more about Vega and Vega-List, refer to the resources and examples. -==== Vega Editor +==== Vega editor The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Vega-Lite, but does not support any {kib}-specific features like {es} requests and interactive base maps. @@ -308,28 +315,15 @@ The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Veg * https://vega.github.io/vega/docs/[Docs] * https://vega.github.io/vega/examples/[Examples] -==== Elastic blog posts -* https://www.elastic.co/blog/getting-started-with-vega-visualizations-in-kibana[Getting Started with Vega Visualizations in Kibana] -* https://www.elastic.co/blog/custom-vega-visualizations-in-kibana[Custom Vega Visualizations in Kibana] -* https://www.elastic.co/blog/sankey-visualization-with-vega-in-kibana[Sankey Visualization with Vega in Kibana] - - -[[vega-using-vega-and-vegalite-examples]] -==== Using Vega and Vega-Lite examples - -When using https://vega.github.io/vega/examples/[Vega] and -https://vega.github.io/vega-lite/examples/[VegaLite] examples, you may +TIP: When you use the examples, you may need to modify the "data" section to use absolute URL. For example, replace `"url": "data/world-110m.json"` with -`"url": "https://vega.github.io/editor/data/world-110m.json"`. Also, -regular Vega examples use `"autosize": "pad"` layout model, whereas -Kibana uses `fit`. Remove all `autosize`, `width`, and `height` -values. See link:#sizing-and-positioning[sizing and positioning]. +`"url": "https://vega.github.io/editor/data/world-110m.json"`. [[vega-additional-configuration-options]] ==== Additional configuration options -These options are specific to the Kibana. link:#vega-with-a-map[Map support] has +These options are specific to the {kib}. link:#vega-with-a-map[Map support] has additional configuration options. [source,yaml] @@ -351,21 +345,3 @@ additional configuration options. /* the rest of Vega code */ } ---- - -[[vega-sizing-and-positioning]] -==== Sizing and positioning - -[[vega-and-vegalite]] -===== Vega and Vega-Lite - -By default, Kibana Vega graphs will use -`autosize = { type: 'fit', contains: 'padding' }` layout model for Vega -and Vega-Lite graphs. The `fit` model uses all available space, ignores -`width` and `height` values, but respects the padding values. You may -override this behaviour by specifying a different `autosize` value. - -[[vega-on-a-map]] -===== Vega on a map - -All Vega graphs will ignore `autosize`, `width`, `height`, and `padding` -values, using `fit` model with zero padding. diff --git a/docs/visualize/visualize_rollup_data.asciidoc b/docs/visualize/visualize_rollup_data.asciidoc deleted file mode 100644 index 481cbc6e39418..0000000000000 --- a/docs/visualize/visualize_rollup_data.asciidoc +++ /dev/null @@ -1,43 +0,0 @@ -[role="xpack"] -[[visualize-rollup-data]] -== Use rolled up data in a visualization - -beta[] - -You can visualize your rolled up data in a variety of charts, tables, maps, and -more. Most visualizations support rolled up data, with the exception of -Timelion and Vega visualizations. - -To get started, go to *Management > Kibana > Index patterns.* -If a rollup index is detected in the cluster, *Create index pattern* -includes an item for creating a rollup index pattern. - -[role="screenshot"] -image::images/management_create_rollup_menu.png[Create index pattern menu] - -You can match an index pattern to only rolled up data, or mix both rolled up -and raw data to visualize all data together. An index pattern can match only one -rolled up index, not multiple. There is no restriction on the number of standard -indices that an index pattern can match. When matching multiple indices, -use a comma to separate the names, with no space after the comma. - -Keep the following in mind when creating a visualization from rolled up data: - -* The data in a rollup index only has summarized metrics for specific fields. -You can’t search any other field from the original raw data. -* Data is summarized into time buckets that might be split into sub buckets for -numeric field values or terms. You can ask for a time aggregation that takes -several time buckets and combines them to lower granularity. For example, -if the rollup job was aggregated by hours, you can ask for buckets of days. - -The following visualization of rolled up data shows the date histogram -interval multiple and the limited metrics aggregations. - -[role="screenshot"] -image::images/management_rollups_visualization.png[][Rollups in visualizations] - -Dashboards can have a mixture of rollup visualizations and regular visualizations, -as shown in the following figure. Note that not all queries and filters support rollups. - -[role="screenshot"] -image::images/management_rolled_dashboard.png[][Rollups in dashboards] diff --git a/examples/state_containers_examples/public/todo/app.tsx b/examples/state_containers_examples/public/todo/app.tsx index 319680d07f9bc..f2183613e4a12 100644 --- a/examples/state_containers_examples/public/todo/app.tsx +++ b/examples/state_containers_examples/public/todo/app.tsx @@ -20,7 +20,7 @@ import { AppMountParameters } from 'kibana/public'; import ReactDOM from 'react-dom'; import React from 'react'; -import { createHashHistory, createBrowserHistory } from 'history'; +import { createHashHistory } from 'history'; import { TodoAppPage } from './todo'; export interface AppOptions { @@ -35,13 +35,10 @@ export enum History { } export const renderApp = ( - { appBasePath, element }: AppMountParameters, + { appBasePath, element, history: platformHistory }: AppMountParameters, { appInstanceId, appTitle, historyType }: AppOptions ) => { - const history = - historyType === History.Browser - ? createBrowserHistory({ basename: appBasePath }) - : createHashHistory(); + const history = historyType === History.Browser ? platformHistory : createHashHistory(); ReactDOM.render( = ({ filter }) => { return ( <>
- + All - + Completed - + Not Completed @@ -121,6 +124,7 @@ const TodoApp: React.FC = ({ filter }) => { }); }} label={todo.text} + data-test-subj={`todoCheckbox-${todo.id}`} /> { - const history = createBrowserHistory({ basename: appBasePath }); const kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); ReactDOM.render( diff --git a/package.json b/package.json index 21e9f67e6206a..956ad9ee4f71d 100644 --- a/package.json +++ b/package.json @@ -120,10 +120,10 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@elastic/apm-rum": "^5.1.1", - "@elastic/charts": "18.3.0", + "@elastic/charts": "18.4.1", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.4.0", @@ -185,7 +185,7 @@ "glob-all": "^3.2.1", "globby": "^8.0.1", "h2o2": "^8.1.2", - "handlebars": "4.5.3", + "handlebars": "4.7.6", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", "history": "^4.9.0", diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index b82c8d0fac897..1a2f6941c2020 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -15,6 +15,7 @@ "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-filter-imports": "^3.0.0", "babel-plugin-styled-components": "^1.10.6", - "babel-plugin-transform-define": "^1.3.1" + "babel-plugin-transform-define": "^1.3.1", + "babel-plugin-transform-imports": "^2.0.0" } } diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index d76a3e9714838..2c1129f275bfe 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -42,5 +42,24 @@ module.exports = () => { }, ], ], + // NOTE: we can enable this by default for everything as soon as we only have one instance + // of lodash across the entire project. For now we are just enabling it for siem + // as they are extensively using the lodash v4 + overrides: [ + { + test: [/x-pack[\/\\]legacy[\/\\]plugins[\/\\]siem[\/\\]public/], + plugins: [ + [ + require.resolve('babel-plugin-transform-imports'), + { + 'lodash/?(((\\w*)?/?)*)': { + transform: 'lodash/${1}/${member}', + preventFullImport: false, + }, + }, + ], + ], + }, + ], }; }; diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 47a0f5f7a5491..5ab59d1c02077 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -18,6 +18,7 @@ */ import { schema } from '..'; +import { TypeOf } from './object_type'; test('returns value by default', () => { const type = schema.object({ @@ -350,3 +351,26 @@ test('unknowns = `ignore` affects only own keys', () => { }) ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); }); + +test('handles optional properties', () => { + const type = schema.object({ + required: schema.string(), + optional: schema.maybe(schema.string()), + }); + + type SchemaType = TypeOf; + + let foo: SchemaType = { + required: 'foo', + }; + foo = { + required: 'hello', + optional: undefined, + }; + foo = { + required: 'hello', + optional: 'bar', + }; + + expect(foo).toBeDefined(); +}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index 5a50e714a5931..fee2d02c1bfb9 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -26,9 +26,26 @@ export type Props = Record>; export type TypeOf> = RT['type']; +type OptionalProperties = Pick< + Base, + { + [Key in keyof Base]: undefined extends TypeOf ? Key : never; + }[keyof Base] +>; + +type RequiredProperties = Pick< + Base, + { + [Key in keyof Base]: undefined extends TypeOf ? never : Key; + }[keyof Base] +>; + // Because of https://github.com/Microsoft/TypeScript/issues/14041 // this might not have perfect _rendering_ output, but it will be typed. -export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeOf }>; +export type ObjectResultType

= Readonly< + { [K in keyof OptionalProperties

]?: TypeOf } & + { [K in keyof RequiredProperties

]: TypeOf } +>; interface UnknownOptions { /** @@ -40,9 +57,7 @@ interface UnknownOptions { unknowns?: 'allow' | 'ignore' | 'forbid'; } -export type ObjectTypeOptions

= TypeOptions< - { [K in keyof P]: TypeOf } -> & +export type ObjectTypeOptions

= TypeOptions> & UnknownOptions; export class ObjectType

extends Type> { diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index dcb4dcd35698d..c0bb408d60d8f 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -44,6 +44,11 @@ run( throw createFlagError('expected --cache to have no value'); } + const includeCoreBundle = flags.core ?? true; + if (typeof includeCoreBundle !== 'boolean') { + throw createFlagError('expected --core to have no value'); + } + const dist = flags.dist ?? false; if (typeof dist !== 'boolean') { throw createFlagError('expected --dist to have no value'); @@ -87,6 +92,7 @@ run( profileWebpack, extraPluginScanDirs, inspectWorkers, + includeCoreBundle, }); await runOptimizer(config) @@ -95,9 +101,10 @@ run( }, { flags: { - boolean: ['watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], + boolean: ['core', 'watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], string: ['workers', 'scan-dir'], default: { + core: true, examples: true, cache: true, 'inspect-workers': true, @@ -107,6 +114,7 @@ run( --workers max number of workers to use --oss only build oss plugins --profile profile the webpack builds and write stats.json files to build outputs + --no-core disable generating the core bundle --no-cache disable the cache --no-examples don't build the example plugins --dist create bundles that are suitable for inclusion in the Kibana distributable diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index f1bc0965a46cc..7581b90d60af2 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -23,7 +23,7 @@ import { BundleCache } from './bundle_cache'; import { UnknownVals } from './ts_helpers'; import { includes, ascending, entriesToObject } from './array_helpers'; -const VALID_BUNDLE_TYPES = ['plugin' as const]; +const VALID_BUNDLE_TYPES = ['plugin' as const, 'entry' as const]; export interface BundleSpec { readonly type: typeof VALID_BUNDLE_TYPES[0]; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 4b4bb1282d939..fe0f75c05c646 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,6 @@ OptimizerConfig { } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { - expect( - getBundles( - [ - { - directory: '/repo/plugins/foo', - id: 'foo', - isUiPlugin: true, - }, - { - directory: '/repo/plugins/bar', - id: 'bar', - isUiPlugin: false, - }, - { - directory: '/outside/of/repo/plugins/baz', - id: 'baz', - isUiPlugin: true, - }, - ], - '/repo' - ).map(b => b.toSpec()) - ).toMatchInlineSnapshot(` - Array [ - Object { - "contextDir": /plugins/foo, - "entry": "./public/index", - "id": "foo", - "outputDir": /plugins/foo/target/public, - "sourceRoot": , - "type": "plugin", - }, - Object { - "contextDir": "/outside/of/repo/plugins/baz", - "entry": "./public/index", - "id": "baz", - "outputDir": "/outside/of/repo/plugins/baz/target/public", - "sourceRoot": , - "type": "plugin", - }, - ] - `); -}); diff --git a/packages/kbn-optimizer/src/optimizer/get_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_bundles.ts deleted file mode 100644 index 7cd7bf15317e0..0000000000000 --- a/packages/kbn-optimizer/src/optimizer/get_bundles.ts +++ /dev/null @@ -1,40 +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. - */ - -import Path from 'path'; - -import { Bundle } from '../common'; - -import { KibanaPlatformPlugin } from './kibana_platform_plugins'; - -export function getBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { - return plugins - .filter(p => p.isUiPlugin) - .map( - p => - new Bundle({ - type: 'plugin', - id: p.id, - entry: './public/index', - sourceRoot: repoRoot, - contextDir: p.directory, - outputDir: Path.resolve(p.directory, 'target/public'), - }) - ); -} diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts new file mode 100644 index 0000000000000..36dc0ca64c6ca --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -0,0 +1,68 @@ +/* + * 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. + */ + +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { getPluginBundles } from './get_plugin_bundles'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo')); + +it('returns a bundle for core and each plugin', () => { + expect( + getPluginBundles( + [ + { + directory: '/repo/plugins/foo', + id: 'foo', + isUiPlugin: true, + }, + { + directory: '/repo/plugins/bar', + id: 'bar', + isUiPlugin: false, + }, + { + directory: '/outside/of/repo/plugins/baz', + id: 'baz', + isUiPlugin: true, + }, + ], + '/repo' + ).map(b => b.toSpec()) + ).toMatchInlineSnapshot(` + Array [ + Object { + "contextDir": /plugins/foo, + "entry": "./public/index", + "id": "foo", + "outputDir": /plugins/foo/target/public, + "sourceRoot": , + "type": "plugin", + }, + Object { + "contextDir": "/outside/of/repo/plugins/baz", + "entry": "./public/index", + "id": "baz", + "outputDir": "/outside/of/repo/plugins/baz/target/public", + "sourceRoot": , + "type": "plugin", + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts new file mode 100644 index 0000000000000..4741cc3c30af7 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -0,0 +1,40 @@ +/* + * 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. + */ + +import Path from 'path'; + +import { Bundle } from '../common'; + +import { KibanaPlatformPlugin } from './kibana_platform_plugins'; + +export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + return plugins + .filter(p => p.isUiPlugin) + .map( + p => + new Bundle({ + type: 'plugin', + id: p.id, + entry: './public/index', + sourceRoot: repoRoot, + contextDir: p.directory, + outputDir: Path.resolve(p.directory, 'target/public'), + }) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index cc564dd4a8387..d4152133f289d 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -19,7 +19,7 @@ jest.mock('./assign_bundles_to_workers.ts'); jest.mock('./kibana_platform_plugins.ts'); -jest.mock('./get_bundles.ts'); +jest.mock('./get_plugin_bundles.ts'); import Path from 'path'; import Os from 'os'; @@ -90,6 +90,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -114,6 +115,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -138,6 +140,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -164,6 +167,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -187,6 +191,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -210,6 +215,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -230,6 +236,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -250,6 +257,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -271,6 +279,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -292,6 +301,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -314,7 +324,7 @@ describe('OptimizerConfig::create()', () => { .assignBundlesToWorkers; const findKibanaPlatformPlugins: jest.Mock = jest.requireMock('./kibana_platform_plugins.ts') .findKibanaPlatformPlugins; - const getBundles: jest.Mock = jest.requireMock('./get_bundles.ts').getBundles; + const getPluginBundles: jest.Mock = jest.requireMock('./get_plugin_bundles.ts').getPluginBundles; beforeEach(() => { if ('mock' in OptimizerConfig.parseOptions) { @@ -326,7 +336,7 @@ describe('OptimizerConfig::create()', () => { { config: Symbol('worker config 2') }, ]); findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins')); - getBundles.mockReturnValue(Symbol('bundles')); + getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({ cache: Symbol('parsed cache'), @@ -348,7 +358,10 @@ describe('OptimizerConfig::create()', () => { expect(config).toMatchInlineSnapshot(` OptimizerConfig { - "bundles": Symbol(bundles), + "bundles": Array [ + Symbol(bundle1), + Symbol(bundle2), + ], "cache": Symbol(parsed cache), "dist": Symbol(parsed dist), "inspectWorkers": Symbol(parsed inspect workers), @@ -383,7 +396,7 @@ describe('OptimizerConfig::create()', () => { } `); - expect(getBundles.mock).toMatchInlineSnapshot(` + expect(getPluginBundles.mock).toMatchInlineSnapshot(` Object { "calls": Array [ Array [ @@ -400,7 +413,10 @@ describe('OptimizerConfig::create()', () => { "results": Array [ Object { "type": "return", - "value": Symbol(bundles), + "value": Array [ + Symbol(bundle1), + Symbol(bundle2), + ], }, ], } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 7e1514058446b..d6336cf867470 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -23,7 +23,7 @@ import Os from 'os'; import { Bundle, WorkerConfig } from '../common'; import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; -import { getBundles } from './get_bundles'; +import { getPluginBundles } from './get_plugin_bundles'; function pickMaxWorkerCount(dist: boolean) { // don't break if cpus() returns nothing, or an empty array @@ -60,6 +60,9 @@ interface Options { pluginScanDirs?: string[]; /** absolute paths that should be added to the default scan dirs */ extraPluginScanDirs?: string[]; + + /** flag that causes the core bundle to be built along with plugins */ + includeCoreBundle?: boolean; } interface ParsedOptions { @@ -72,6 +75,7 @@ interface ParsedOptions { pluginPaths: string[]; pluginScanDirs: string[]; inspectWorkers: boolean; + includeCoreBundle: boolean; } export class OptimizerConfig { @@ -83,6 +87,7 @@ export class OptimizerConfig { const profileWebpack = !!options.profileWebpack; const inspectWorkers = !!options.inspectWorkers; const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE; + const includeCoreBundle = !!options.includeCoreBundle; const repoRoot = options.repoRoot; if (!Path.isAbsolute(repoRoot)) { @@ -134,13 +139,28 @@ export class OptimizerConfig { pluginScanDirs, pluginPaths, inspectWorkers, + includeCoreBundle, }; } static create(inputOptions: Options) { const options = OptimizerConfig.parseOptions(inputOptions); const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); - const bundles = getBundles(plugins, options.repoRoot); + const bundles = [ + ...(options.includeCoreBundle + ? [ + new Bundle({ + type: 'entry', + id: 'core', + entry: './public/entry_point', + sourceRoot: options.repoRoot, + contextDir: Path.resolve(options.repoRoot, 'src/core'), + outputDir: Path.resolve(options.repoRoot, 'src/core/target/public'), + }), + ] + : []), + ...getPluginBundles(plugins, options.repoRoot), + ]; return new OptimizerConfig( bundles, diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 2f81d92ec923c..7411e2df1b613 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -34,7 +34,6 @@ import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../c const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); -const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const STATIC_BUNDLE_PLUGINS = [ @@ -104,8 +103,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { output: { path: bundle.outputDir, - filename: '[name].plugin.js', - publicPath: PUBLIC_PATH_PLACEHOLDER, + filename: `[name].${bundle.type}.js`, devtoolModuleFilenameTemplate: info => `/${bundle.type}:${bundle.id}/${Path.relative( bundle.sourceRoot, @@ -140,12 +138,22 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { module: { // no parse rules for a few known large packages which have no require() statements + // or which have require() statements that should be ignored because the file is + // already bundled with all its necessary depedencies noParse: [ /[\///]node_modules[\///]elasticsearch-browser[\///]/, - /[\///]node_modules[\///]lodash[\///]index\.js/, + /[\///]node_modules[\///]lodash[\///]index\.js$/, + /[\///]node_modules[\///]vega-lib[\///]build[\///]vega\.js$/, ], rules: [ + { + include: Path.join(bundle.contextDir, bundle.entry), + loader: UiSharedDeps.publicPathLoader, + options: { + key: bundle.id, + }, + }, { test: /\.css$/, include: /node_modules/, @@ -289,6 +297,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { resolve: { extensions: ['.js', '.ts', '.tsx', '.json'], + mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), }, diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 1531c1d22b01b..779d8a4153644 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -49,6 +49,13 @@ module.exports = async ({ config }) => { }, }); + config.module.rules.push({ + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', + }, + }); + // Handle Typescript files config.module.rules.push({ test: /\.tsx?$/, diff --git a/packages/kbn-ui-shared-deps/index.d.ts b/packages/kbn-ui-shared-deps/index.d.ts index dec519da69641..b829c87d91c4a 100644 --- a/packages/kbn-ui-shared-deps/index.d.ts +++ b/packages/kbn-ui-shared-deps/index.d.ts @@ -53,3 +53,8 @@ export const lightCssDistFilename: string; export const externals: { [key: string]: string; }; + +/** + * Webpack loader for configuring the public path lookup from `window.__kbnPublicPath__`. + */ +export const publicPathLoader: string; diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 666ec7a46ff06..42ed08259ac8f 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -64,3 +64,4 @@ exports.externals = { 'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser', 'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser', }; +exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index c8614b1df9d5d..46f55da87575d 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,8 +9,8 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "18.3.0", - "@elastic/eui": "21.0.1", + "@elastic/charts": "18.4.1", + "@elastic/eui": "22.3.0", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js new file mode 100644 index 0000000000000..6b7a27c9ca52b --- /dev/null +++ b/packages/kbn-ui-shared-deps/public_path_loader.js @@ -0,0 +1,23 @@ +/* + * 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. + */ + +module.exports = function(source) { + const options = this.query; + return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`; +}; diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index a875274544905..bf63c57765859 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -46,7 +46,6 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ path: UiSharedDeps.distDir, filename: '[name].js', sourceMapFilename: '[file].map', - publicPath: '__REPLACE_WITH_PUBLIC_PATH__', devtoolModuleFilenameTemplate: info => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', @@ -55,6 +54,17 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ module: { noParse: [MOMENT_SRC], rules: [ + { + include: [require.resolve('./entry.js')], + use: [ + { + loader: UiSharedDeps.publicPathLoader, + options: { + key: 'kbn-ui-shared-deps', + }, + }, + ], + }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], diff --git a/scripts/prettier_on_changed.js b/scripts/prettier_on_changed.js new file mode 100644 index 0000000000000..f9598110f91fd --- /dev/null +++ b/scripts/prettier_on_changed.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +require('../src/setup_node_env/babel_register'); +require('../src/dev/run_prettier_on_changed'); diff --git a/src/cli/cluster/run_kbn_optimizer.ts b/src/cli/cluster/run_kbn_optimizer.ts index 7752d4a45ab65..b811fc1f6b294 100644 --- a/src/cli/cluster/run_kbn_optimizer.ts +++ b/src/cli/cluster/run_kbn_optimizer.ts @@ -34,6 +34,7 @@ export function runKbnOptimizer(opts: Record, config: LegacyConfig) const optimizerConfig = OptimizerConfig.create({ repoRoot: REPO_ROOT, watch: true, + includeCoreBundle: true, oss: !!opts.oss, examples: !!opts.runExamples, pluginPaths: config.get('plugins.paths'), diff --git a/src/core/public/core.css b/src/core/public/_core.scss similarity index 100% rename from src/core/public/core.css rename to src/core/public/_core.scss diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 61c8bc3cadae5..4c135c5769067 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -21,7 +21,7 @@ import React, { FunctionComponent, useMemo } from 'react'; import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom'; import { History } from 'history'; import { Observable } from 'rxjs'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { AppLeaveHandler, AppStatus, Mounter } from '../types'; import { AppContainer } from './app_container'; diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 94fa74f4bd861..a42719417a2b1 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -59,7 +59,6 @@ const defaultCoreSystemParams = { warnLegacyBrowsers: true, }, } as any, - requireLegacyFiles: jest.fn(), }; beforeEach(() => { @@ -104,19 +103,22 @@ describe('constructor', () => { }); }); - it('passes requireLegacyFiles, useLegacyTestHarness, and a dom element to LegacyPlatformService', () => { + it('passes required params to LegacyPlatformService', () => { const requireLegacyFiles = { requireLegacyFiles: true }; - const useLegacyTestHarness = { useLegacyTestHarness: true }; + const requireLegacyBootstrapModule = { requireLegacyBootstrapModule: true }; + const requireNewPlatformShimModule = { requireNewPlatformShimModule: true }; createCoreSystem({ requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, }); expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1); expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({ requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 5c10d89459128..e58114b69dcc1 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -17,8 +17,6 @@ * under the License. */ -import './core.css'; - import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; import { CoreSetup, CoreStart } from '.'; @@ -50,8 +48,9 @@ interface Params { rootDomElement: HTMLElement; browserSupportsCsp: boolean; injectedMetadata: InjectedMetadataParams['injectedMetadata']; - requireLegacyFiles: LegacyPlatformParams['requireLegacyFiles']; - useLegacyTestHarness?: LegacyPlatformParams['useLegacyTestHarness']; + requireLegacyFiles?: LegacyPlatformParams['requireLegacyFiles']; + requireLegacyBootstrapModule?: LegacyPlatformParams['requireLegacyBootstrapModule']; + requireNewPlatformShimModule?: LegacyPlatformParams['requireNewPlatformShimModule']; } /** @internal */ @@ -111,7 +110,8 @@ export class CoreSystem { browserSupportsCsp, injectedMetadata, requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, } = params; this.rootDomElement = rootDomElement; @@ -145,7 +145,8 @@ export class CoreSystem { this.legacy = new LegacyPlatformService({ requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, }); } diff --git a/src/core/public/entry_point.ts b/src/core/public/entry_point.ts new file mode 100644 index 0000000000000..9461acccf30b9 --- /dev/null +++ b/src/core/public/entry_point.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * This is the entry point used to boot the frontend when serving a application + * that lives in the Kibana Platform. + * + * Any changes to this file should be kept in sync with + * src/legacy/ui/ui_bundles/app_entry_template.js + */ + +import './index.scss'; +import { i18n } from '@kbn/i18n'; +import { CoreSystem } from './core_system'; + +const injectedMetadata = JSON.parse( + document.querySelector('kbn-injected-metadata')!.getAttribute('data')! +); + +if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && process.env.ELASTIC_APM_ACTIVE === 'true') { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { init } = require('@elastic/apm-rum'); + init(injectedMetadata.vars.apmConfig); +} + +i18n + .load(injectedMetadata.i18n.translationsUrl) + .catch(e => e) + .then(async i18nError => { + const coreSystem = new CoreSystem({ + injectedMetadata, + rootDomElement: document.body, + browserSupportsCsp: !(window as any).__kbnCspNotEnforced__, + }); + + const setup = await coreSystem.setup(); + if (i18nError && setup) { + setup.fatalErrors.add(i18nError); + } + + await coreSystem.start(); + }); diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts index d4aced6894526..3cd8ef6169090 100644 --- a/src/core/public/http/index.ts +++ b/src/core/public/http/index.ts @@ -18,4 +18,5 @@ */ export { HttpService } from './http_service'; +export { HttpFetchError } from './http_fetch_error'; export * from './types'; diff --git a/src/core/public/index.scss b/src/core/public/index.scss index 86f2efdff7702..4be46899cff67 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -1,11 +1,11 @@ -// Functions need to be first, since we use them in our variables and mixin definitions -@import '@elastic/eui/src/global_styling/functions/index'; - -// Variables come next, and are used in some mixins -@import '@elastic/eui/src/global_styling/variables/index'; - -// Mixins provide generic code expansion through helpers -@import '@elastic/eui/src/global_styling/mixins/index'; +// This file is built by both the legacy and KP build systems so we need to +// import this explicitly +@import '../../legacy/ui/public/styles/_styling_constants'; +@import './core'; @import './chrome/index'; @import './overlays/index'; +@import './rendering/index'; + +// Global styles need to be migrated +@import '../../legacy/ui/public/styles/_legacy/_index'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 254cac3495599..b4f64125a03ef 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -143,6 +143,7 @@ export { export { HttpHeadersInit, HttpRequestInit, + HttpFetchError, HttpFetchOptions, HttpFetchOptionsWithPath, HttpFetchQuery, diff --git a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap deleted file mode 100644 index 97629fdd1add5..0000000000000 --- a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#start() load order useLegacyTestHarness = false loads ui/modules before ui/chrome, and both before legacy files 1`] = ` -Array [ - "ui/new_platform", - "ui/chrome", - "legacy files", -] -`; - -exports[`#start() load order useLegacyTestHarness = true loads ui/modules before ui/test_harness, and both before legacy files 1`] = ` -Array [ - "ui/new_platform", - "ui/test_harness", - "legacy files", -] -`; - -exports[`#stop() destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement 1`] = ` -

-`; - -exports[`#stop() does nothing if angular was not bootstrapped to targetDomElement 1`] = ` -
- - -

- this should not be removed -

- - -
-`; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index c3de645c6b17e..fa29320aab4e6 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -19,34 +19,6 @@ import angular from 'angular'; -const mockLoadOrder: string[] = []; - -const mockUiNewPlatformSetup = jest.fn(); -const mockUiNewPlatformStart = jest.fn(); -jest.mock('ui/new_platform', () => { - mockLoadOrder.push('ui/new_platform'); - return { - __setup__: mockUiNewPlatformSetup, - __start__: mockUiNewPlatformStart, - }; -}); - -const mockUiChromeBootstrap = jest.fn(); -jest.mock('ui/chrome', () => { - mockLoadOrder.push('ui/chrome'); - return { - bootstrap: mockUiChromeBootstrap, - }; -}); - -const mockUiTestHarnessBootstrap = jest.fn(); -jest.mock('ui/test_harness', () => { - mockLoadOrder.push('ui/test_harness'); - return { - bootstrap: mockUiTestHarnessBootstrap, - }; -}); - import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -69,10 +41,24 @@ const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); const notificationsSetup = notificationServiceMock.createSetupContract(); const uiSettingsSetup = uiSettingsServiceMock.createSetupContract(); +const mockLoadOrder: string[] = []; +const mockUiNewPlatformSetup = jest.fn(); +const mockUiNewPlatformStart = jest.fn(); +const mockUiChromeBootstrap = jest.fn(); const defaultParams = { requireLegacyFiles: jest.fn(() => { mockLoadOrder.push('legacy files'); }), + requireLegacyBootstrapModule: jest.fn(() => { + mockLoadOrder.push('ui/chrome'); + return { + bootstrap: mockUiChromeBootstrap, + }; + }), + requireNewPlatformShimModule: jest.fn(() => ({ + __setup__: mockUiNewPlatformSetup, + __start__: mockUiNewPlatformStart, + })), }; const defaultSetupDeps = { @@ -128,7 +114,7 @@ afterEach(() => { describe('#setup()', () => { describe('default', () => { - it('initializes ui/new_platform with core APIs', () => { + it('initializes new platform shim module with core APIs', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, }); @@ -138,6 +124,21 @@ describe('#setup()', () => { expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1); expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(expect.any(Object), {}); }); + + it('throws error if requireNewPlatformShimModule is undefined', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + requireNewPlatformShimModule: undefined, + }); + + expect(() => { + legacyPlatform.setup(defaultSetupDeps); + }).toThrowErrorMatchingInlineSnapshot( + `"requireNewPlatformShimModule must be specified when rendering a legacy application"` + ); + + expect(mockUiNewPlatformSetup).not.toHaveBeenCalled(); + }); }); }); @@ -171,6 +172,21 @@ describe('#start()', () => { expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); }); + it('throws error if requireNewPlatformShimeModule is undefined', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + requireNewPlatformShimModule: undefined, + }); + + expect(() => { + legacyPlatform.start(defaultStartDeps); + }).toThrowErrorMatchingInlineSnapshot( + `"requireNewPlatformShimModule must be specified when rendering a legacy application"` + ); + + expect(mockUiNewPlatformStart).not.toHaveBeenCalled(); + }); + it('resolves getStartServices with core and plugin APIs', async () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, @@ -185,67 +201,35 @@ describe('#start()', () => { expect(pluginsStart).toBe(defaultStartDeps.plugins); }); - describe('useLegacyTestHarness = false', () => { - it('passes the targetDomElement to ui/chrome', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); + it('passes the targetDomElement to legacy bootstrap module', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); - expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled(); - expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); - }); + expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); + expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); }); - describe('useLegacyTestHarness = true', () => { - it('passes the targetDomElement to ui/test_harness', () => { + describe('load order', () => { + it('loads ui/modules before ui/chrome, and both before legacy files', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, - useLegacyTestHarness: true, }); + expect(mockLoadOrder).toEqual([]); + legacyPlatform.setup(defaultSetupDeps); legacyPlatform.start(defaultStartDeps); - expect(mockUiChromeBootstrap).not.toHaveBeenCalled(); - expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); - }); - }); - - describe('load order', () => { - describe('useLegacyTestHarness = false', () => { - it('loads ui/modules before ui/chrome, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockLoadOrder).toMatchSnapshot(); - }); - }); - - describe('useLegacyTestHarness = true', () => { - it('loads ui/modules before ui/test_harness, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - useLegacyTestHarness: true, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockLoadOrder).toMatchSnapshot(); - }); + expect(mockLoadOrder).toMatchInlineSnapshot(` + Array [ + "ui/chrome", + "legacy files", + ] + `); }); }); }); @@ -262,7 +246,17 @@ describe('#stop()', () => { }); legacyPlatform.stop(); - expect(targetDomElement).toMatchSnapshot(); + expect(targetDomElement).toMatchInlineSnapshot(` +
+ + +

+ this should not be removed +

+ + +
+ `); }); it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', async () => { @@ -291,7 +285,11 @@ describe('#stop()', () => { legacyPlatform.start({ ...defaultStartDeps, targetDomElement }); legacyPlatform.stop(); - expect(targetDomElement).toMatchSnapshot(); + expect(targetDomElement).toMatchInlineSnapshot(` +
+ `); expect(scopeDestroySpy).toHaveBeenCalledTimes(1); }); }); diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 39ca7bdf54b7c..01837ba6f5940 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -25,8 +25,12 @@ import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../'; /** @internal */ export interface LegacyPlatformParams { - requireLegacyFiles: () => void; - useLegacyTestHarness?: boolean; + requireLegacyFiles?: () => void; + requireLegacyBootstrapModule?: () => BootstrapModule; + requireNewPlatformShimModule?: () => { + __setup__: (legacyCore: LegacyCoreSetup, plugins: Record) => void; + __start__: (legacyCore: LegacyCoreStart, plugins: Record) => void; + }; } interface SetupDeps { @@ -92,7 +96,13 @@ export class LegacyPlatformService { // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts if (core.injectedMetadata.getLegacyMode()) { - require('ui/new_platform').__setup__(legacyCore, plugins); + if (!this.params.requireNewPlatformShimModule) { + throw new Error( + `requireNewPlatformShimModule must be specified when rendering a legacy application` + ); + } + + this.params.requireNewPlatformShimModule().__setup__(legacyCore, plugins); } } @@ -131,16 +141,29 @@ export class LegacyPlatformService { this.startDependencies$.next([legacyCore, plugins, {}]); + if (!this.params.requireNewPlatformShimModule) { + throw new Error( + `requireNewPlatformShimModule must be specified when rendering a legacy application` + ); + } + if (!this.params.requireLegacyBootstrapModule) { + throw new Error( + `requireLegacyBootstrapModule must be specified when rendering a legacy application` + ); + } + // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts - require('ui/new_platform').__start__(legacyCore, plugins); + this.params.requireNewPlatformShimModule().__start__(legacyCore, plugins); // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first - this.bootstrapModule = this.loadBootstrapModule(); + this.bootstrapModule = this.params.requireLegacyBootstrapModule(); // require the files that will tie into the legacy platform - this.params.requireLegacyFiles(); + if (this.params.requireLegacyFiles) { + this.params.requireLegacyFiles(); + } if (!this.bootstrapModule) { throw new Error('Bootstrap module must be loaded before `start`'); @@ -172,20 +195,6 @@ export class LegacyPlatformService { // clear the inner html of the root angular element this.targetDomElement.textContent = ''; } - - private loadBootstrapModule(): BootstrapModule { - if (this.params.useLegacyTestHarness) { - // wrapped in NODE_ENV check so the `ui/test_harness` module - // is not included in the distributable - if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true') { - return require('ui/test_harness'); - } - - throw new Error('tests bundle is not available in the distributable'); - } - - return require('ui/chrome'); - } } const notSupported = (methodName: string) => (...args: any[]) => { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6d95d1bc7069c..b92bb209d2607 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -593,6 +593,23 @@ export type HandlerFunction = (context: T, ...args: any[]) => // @public export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; +// @internal (undocumented) +export class HttpFetchError extends Error implements IHttpFetchError { + constructor(message: string, name: string, request: Request, response?: Response | undefined, body?: any); + // (undocumented) + readonly body?: any; + // (undocumented) + readonly name: string; + // (undocumented) + readonly req: Request; + // (undocumented) + readonly request: Request; + // (undocumented) + readonly res?: Response; + // (undocumented) + readonly response?: Response | undefined; +} + // @public export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss new file mode 100644 index 0000000000000..ff28fc75e367d --- /dev/null +++ b/src/core/public/rendering/_base.scss @@ -0,0 +1,46 @@ +/** + * stretch the root element of the Kibana application to set the base-size that + * flexed children should keep. Only works when paired with root styles applied + * by core service from new platform + */ +// SASSTODO: Naming here is too embedded and high up that changing them could cause major breaks +#kibana-body { + overflow-x: hidden; + min-height: 100%; +} + +.app-wrapper { + display: flex; + flex-flow: column nowrap; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 5; + margin: 0 auto; + + /** + * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state + * when we're looking at the log-in screen. + */ + &.hidden-chrome { + left: 0 !important; /* 1 */ + } + + .navbar-right { + margin-right: 0; + } +} + +.app-wrapper-panel { + display: flex; + flex-grow: 1; + flex-shrink: 0; + flex-basis: auto; + flex-direction: column; + + > * { + flex-shrink: 0; + } +} diff --git a/src/core/public/rendering/_index.scss b/src/core/public/rendering/_index.scss new file mode 100644 index 0000000000000..c8567498b42ec --- /dev/null +++ b/src/core/public/rendering/_index.scss @@ -0,0 +1 @@ +@import './base'; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index afc77806afb91..d26cadecb184a 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -28,12 +28,6 @@ import { SavedObjectsMigrationVersion, } from '../../server'; -// TODO: Migrate to an error modal powered by the NP? -import { - isAutoCreateIndexError, - showAutoCreateIndexErrorPage, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index'; import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; @@ -226,7 +220,9 @@ export class SavedObjectsClient { .then(resp => this.createSavedObject(resp)) .catch((error: object) => { if (isAutoCreateIndexError(error)) { - showAutoCreateIndexErrorPage(); + window.location.assign( + this.http.basePath.prepend('/app/kibana#/error/action.auto_create_index') + ); } throw error; @@ -472,3 +468,9 @@ const renameKeys = , U extends Record ...{ [keysMap[key] || key]: obj[key] }, }; }, {}); + +const isAutoCreateIndexError = (error: any) => { + return ( + error?.res?.status === 503 && error?.body?.attributes?.code === 'ES_AUTO_CREATE_INDEX_ERROR' + ); +}; diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index f0b21adf62ff7..9e098c06ba155 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -128,56 +128,6 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), - // Monitoring renames - // TODO: Remove these from here once the monitoring plugin is migrated to NP - renameFromRoot('xpack.monitoring.enabled', 'monitoring.enabled'), - renameFromRoot('xpack.monitoring.ui.enabled', 'monitoring.ui.enabled'), - renameFromRoot( - 'xpack.monitoring.kibana.collection.enabled', - 'monitoring.kibana.collection.enabled' - ), - renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), - renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), - renameFromRoot( - 'xpack.monitoring.show_license_expiration', - 'monitoring.ui.show_license_expiration' - ), - renameFromRoot( - 'xpack.monitoring.ui.container.elasticsearch.enabled', - 'monitoring.ui.container.elasticsearch.enabled' - ), - renameFromRoot( - 'xpack.monitoring.ui.container.logstash.enabled', - 'monitoring.ui.container.logstash.enabled' - ), - renameFromRoot( - 'xpack.monitoring.tests.cloud_detector.enabled', - 'monitoring.tests.cloud_detector.enabled' - ), - renameFromRoot( - 'xpack.monitoring.kibana.collection.interval', - 'monitoring.kibana.collection.interval' - ), - renameFromRoot('xpack.monitoring.elasticsearch.hosts', 'monitoring.ui.elasticsearch.hosts'), - renameFromRoot('xpack.monitoring.elasticsearch.username', 'monitoring.ui.elasticsearch.username'), - renameFromRoot('xpack.monitoring.elasticsearch.password', 'monitoring.ui.elasticsearch.password'), - renameFromRoot( - 'xpack.monitoring.xpack_api_polling_frequency_millis', - 'monitoring.xpack_api_polling_frequency_millis' - ), - renameFromRoot( - 'xpack.monitoring.cluster_alerts.email_notifications.enabled', - 'monitoring.cluster_alerts.email_notifications.enabled' - ), - renameFromRoot( - 'xpack.monitoring.cluster_alerts.email_notifications.email_address', - 'monitoring.cluster_alerts.email_notifications.email_address' - ), - renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), - renameFromRoot( - 'xpack.monitoring.elasticsearch.logFetchCount', - 'monitoring.ui.elasticsearch.logFetchCount' - ), configPathDeprecation, dataPathDeprecation, rewriteBasePathDeprecation, diff --git a/src/core/server/http/router/validator/validator.ts b/src/core/server/http/router/validator/validator.ts index 6c766e69f0f37..a2299b47ae253 100644 --- a/src/core/server/http/router/validator/validator.ts +++ b/src/core/server/http/router/validator/validator.ts @@ -17,7 +17,14 @@ * under the License. */ -import { ValidationError, Type, schema, ObjectType, isConfigSchema } from '@kbn/config-schema'; +import { + ValidationError, + Type, + schema, + ObjectType, + TypeOf, + isConfigSchema, +} from '@kbn/config-schema'; import { Stream } from 'stream'; import { RouteValidationError } from './validator_error'; @@ -85,7 +92,7 @@ type RouteValidationResultType | undefined> = T extends RouteValidationFunction ? ReturnType['value'] : T extends Type - ? ReturnType + ? TypeOf : undefined >; @@ -170,7 +177,7 @@ export class RouteValidator

{ * @internal */ public getParams(data: unknown, namespace?: string): Readonly

{ - return this.validate(this.config.params, this.options.unsafe?.params, data, namespace); + return this.validate(this.config.params, this.options.unsafe?.params, data, namespace) as P; } /** @@ -178,7 +185,7 @@ export class RouteValidator

{ * @internal */ public getQuery(data: unknown, namespace?: string): Readonly { - return this.validate(this.config.query, this.options.unsafe?.query, data, namespace); + return this.validate(this.config.query, this.options.unsafe?.query, data, namespace) as Q; } /** @@ -186,7 +193,7 @@ export class RouteValidator

{ * @internal */ public getBody(data: unknown, namespace?: string): Readonly { - return this.validate(this.config.body, this.options.unsafe?.body, data, namespace); + return this.validate(this.config.body, this.options.unsafe?.body, data, namespace) as B; } /** diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ef57fae159d7e..86192245bd2d1 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -230,6 +230,7 @@ export { SavedObjectsMigrationLogger, SavedObjectsRawDoc, SavedObjectSanitizedDoc, + SavedObjectUnsanitizedDoc, SavedObjectsRepositoryFactory, SavedObjectsResolveImportErrorsOptions, SavedObjectsSchema, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 3b9a39db72278..2451b98ffdf29 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -45,6 +45,7 @@ export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service. export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; +export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7ca5c75f19e8f..6369720ada2c3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1679,8 +1679,6 @@ export interface SavedObjectMigrationContext { log: SavedObjectsMigrationLogger; } -// Warning: (ae-forgotten-export) The symbol "SavedObjectUnsanitizedDoc" needs to be exported by the entry point index.d.ts -// // @public export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; @@ -2314,6 +2312,9 @@ export class SavedObjectTypeRegistry { registerType(type: SavedObjectsType): void; } +// @public +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; + // @public export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.js b/src/dev/build/tasks/build_kibana_platform_plugins.js index 101d6bd15fc10..79cd698e4782c 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.js +++ b/src/dev/build/tasks/build_kibana_platform_plugins.js @@ -29,6 +29,7 @@ export const BuildKibanaPlatformPluginsTask = { examples: false, watch: false, dist: true, + includeCoreBundle: true, }); await runOptimizer(optimizerConfig) diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 52928d6e47fc4..8e8d69a4dfefa 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -48,12 +48,12 @@ export const CleanClientModulesOnDLLTask = { ]; const discoveredLegacyCorePluginEntries = await globby([ `${baseDir}/src/legacy/core_plugins/*/index.js`, - // Small exception to load dynamically discovered functions for timelion plugin - `${baseDir}/src/legacy/core_plugins/timelion/server/*_functions/**/*.js`, `!${baseDir}/src/legacy/core_plugins/**/public`, ]); const discoveredPluginEntries = await globby([ `${baseDir}/src/plugins/*/server/index.js`, + // Small exception to load dynamically discovered functions for timelion plugin + `${baseDir}/src/plugins/vis_type_timelion/server/*_functions/**/*.js`, `!${baseDir}/src/plugins/**/public`, ]); const discoveredNewPlatformXpackPlugins = await globby([ 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 38acfb15d3ece..eb7a121c2e64b 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 @@ -72,6 +72,22 @@ kibana_vars=( map.tilemap.options.minZoom map.tilemap.options.subdomains map.tilemap.url + monitoring.cluster_alerts.email_notifications.email_address + monitoring.enabled + monitoring.kibana.collection.enabled + monitoring.kibana.collection.interval + monitoring.ui.container.elasticsearch.enabled + monitoring.ui.container.logstash.enabled + monitoring.ui.elasticsearch.password + monitoring.ui.elasticsearch.pingTimeout + monitoring.ui.elasticsearch.hosts + monitoring.ui.elasticsearch.username + monitoring.ui.elasticsearch.logFetchCount + monitoring.ui.elasticsearch.ssl.certificateAuthorities + monitoring.ui.elasticsearch.ssl.verificationMode + monitoring.ui.enabled + monitoring.ui.max_bucket_size + monitoring.ui.min_interval_seconds newsfeed.enabled ops.interval path.data @@ -160,25 +176,6 @@ kibana_vars=( xpack.infra.sources.default.metricAlias xpack.license_management.enabled xpack.ml.enabled - xpack.monitoring.cluster_alerts.email_notifications.email_address - xpack.monitoring.elasticsearch.password - xpack.monitoring.elasticsearch.pingTimeout - xpack.monitoring.elasticsearch.hosts - xpack.monitoring.elasticsearch.username - xpack.monitoring.elasticsearch.logFetchCount - xpack.monitoring.elasticsearch.ssl.certificateAuthorities - xpack.monitoring.elasticsearch.ssl.verificationMode - xpack.monitoring.enabled - xpack.monitoring.kibana.collection.enabled - xpack.monitoring.kibana.collection.interval - xpack.monitoring.max_bucket_size - xpack.monitoring.min_interval_seconds - xpack.monitoring.node_resolver - xpack.monitoring.report_stats - xpack.monitoring.elasticsearch.pingTimeout - xpack.monitoring.ui.container.elasticsearch.enabled - xpack.monitoring.ui.container.logstash.enabled - xpack.monitoring.ui.enabled xpack.reporting.capture.browser.autoDownload xpack.reporting.capture.browser.chromium.disableSandbox xpack.reporting.capture.browser.chromium.inspect @@ -197,10 +194,14 @@ kibana_vars=( xpack.reporting.csv.checkForFormulas xpack.reporting.csv.escapeFormulaValues xpack.reporting.csv.enablePanelActionDownload + xpack.reporting.csv.useByteOrderMarkEncoding xpack.reporting.csv.maxSizeBytes xpack.reporting.csv.scroll.duration xpack.reporting.csv.scroll.size xpack.reporting.capture.maxAttempts + xpack.reporting.capture.timeouts.openUrl + xpack.reporting.capture.timeouts.waitForElements + xpack.reporting.capture.timeouts.renderComplete xpack.reporting.enabled xpack.reporting.encryptionKey xpack.reporting.index diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js index 69df33f7f2e11..c80f9334cfaeb 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js @@ -24,12 +24,12 @@ function generator({ imageFlavor }) { # # ** THIS IS AN AUTO-GENERATED FILE ** # - + # Default Kibana configuration for docker target server.name: kibana server.host: "0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] - ${!imageFlavor ? 'xpack.monitoring.ui.container.elasticsearch.enabled: true' : ''} + ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} `); } diff --git a/src/dev/eslint/index.js b/src/dev/eslint/index.ts similarity index 100% rename from src/dev/eslint/index.js rename to src/dev/eslint/index.ts diff --git a/src/dev/eslint/lint_files.js b/src/dev/eslint/lint_files.js deleted file mode 100644 index a76edeb2eb865..0000000000000 --- a/src/dev/eslint/lint_files.js +++ /dev/null @@ -1,58 +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. - */ - -import { CLIEngine } from 'eslint'; - -import { createFailError } from '@kbn/dev-utils'; -import { REPO_ROOT } from '../constants'; - -/** - * Lints a list of files with eslint. eslint reports are written to the log - * and a FailError is thrown when linting errors occur. - * - * @param {ToolingLog} log - * @param {Array} files - * @return {undefined} - */ -export function lintFiles(log, files, { fix } = {}) { - const cli = new CLIEngine({ - cache: true, - cwd: REPO_ROOT, - fix, - }); - - const paths = files.map(file => file.getRelativePath()); - const report = cli.executeOnFiles(paths); - - if (fix) { - CLIEngine.outputFixes(report); - } - - const failTypes = []; - if (report.errorCount > 0) failTypes.push('errors'); - if (report.warningCount > 0) failTypes.push('warning'); - - if (!failTypes.length) { - log.success('[eslint] %d files linted successfully', files.length); - return; - } - - log.error(cli.getFormatter()(report.results)); - throw createFailError(`[eslint] ${failTypes.join(' & ')}`); -} diff --git a/src/dev/eslint/lint_files.ts b/src/dev/eslint/lint_files.ts new file mode 100644 index 0000000000000..80c493233f39a --- /dev/null +++ b/src/dev/eslint/lint_files.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +import { CLIEngine } from 'eslint'; + +import { createFailError, ToolingLog } from '@kbn/dev-utils'; +import { File } from '../file'; +import { REPO_ROOT } from '../constants'; + +/** + * Lints a list of files with eslint. eslint reports are written to the log + * and a FailError is thrown when linting errors occur. + * + * @param {ToolingLog} log + * @param {Array} files + * @return {undefined} + */ +export function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) { + const cli = new CLIEngine({ + cache: true, + cwd: REPO_ROOT, + fix, + }); + + const paths = files.map(file => file.getRelativePath()); + const report = cli.executeOnFiles(paths); + + if (fix) { + CLIEngine.outputFixes(report); + } + + const failTypes = []; + if (report.errorCount > 0) failTypes.push('errors'); + if (report.warningCount > 0) failTypes.push('warning'); + + if (!failTypes.length) { + log.success('[eslint] %d files linted successfully', files.length); + return; + } + + log.error(cli.getFormatter()(report.results)); + throw createFailError(`[eslint] ${failTypes.join(' & ')}`); +} diff --git a/src/dev/eslint/pick_files_to_lint.js b/src/dev/eslint/pick_files_to_lint.js deleted file mode 100644 index e3212c00d9e0d..0000000000000 --- a/src/dev/eslint/pick_files_to_lint.js +++ /dev/null @@ -1,47 +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. - */ - -import { CLIEngine } from 'eslint'; - -/** - * Filters a list of files to only include lintable files. - * - * @param {ToolingLog} log - * @param {Array} files - * @return {Array} - */ -export function pickFilesToLint(log, files) { - const cli = new CLIEngine(); - - return files.filter(file => { - if (!file.isJs() && !file.isTypescript()) { - return; - } - - const path = file.getRelativePath(); - - if (cli.isPathIgnored(path)) { - log.warning(`[eslint] %j ignored by .eslintignore`, file); - return false; - } - - log.debug('[eslint] linting %j', file); - return true; - }); -} diff --git a/src/dev/eslint/pick_files_to_lint.ts b/src/dev/eslint/pick_files_to_lint.ts new file mode 100644 index 0000000000000..b96781fc3a611 --- /dev/null +++ b/src/dev/eslint/pick_files_to_lint.ts @@ -0,0 +1,49 @@ +/* + * 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. + */ +import { CLIEngine } from 'eslint'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { File } from '../file'; + +/** + * Filters a list of files to only include lintable files. + * + * @param {ToolingLog} log + * @param {Array} files + * @return {Array} + */ +export function pickFilesToLint(log: ToolingLog, files: File[]) { + const cli = new CLIEngine({}); + + return files.filter(file => { + if (!file.isJs() && !file.isTypescript()) { + return; + } + + const path = file.getRelativePath(); + + if (cli.isPathIgnored(path)) { + log.warning(`[eslint] %j ignored by .eslintignore`, file); + return false; + } + + log.debug('[eslint] linting %j', file); + return true; + }); +} diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 7da14e0dfe51b..43a2cbd78c502 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -63,6 +63,7 @@ export default { '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js', '\\.ace\\.worker.js$': '/src/dev/jest/mocks/ace_worker_module_mock.js', + '^(!!)?file-loader!': '/src/dev/jest/mocks/file_mock.js', }, setupFiles: [ '/src/dev/jest/setup/babel_polyfill.js', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 1b5110a61cbc4..fc95288eabed8 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -116,11 +116,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', - 'src/legacy/core_plugins/timelion/server/lib/asSorted.js', - 'src/legacy/core_plugins/timelion/server/lib/unzipPairs.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/bucketList.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/seriesList.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/tlConfig.js', 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', @@ -164,8 +159,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'webpackShims/elasticsearch-browser.js', 'webpackShims/moment-timezone.js', 'webpackShims/ui-bootstrap.js', - 'x-pack/legacy/plugins/graph/public/graphClientWorkspace.js', - 'x-pack/legacy/plugins/graph/public/angular-venn-simple.js', 'x-pack/legacy/plugins/index_management/public/lib/editSettings.js', 'x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js', 'x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', diff --git a/src/dev/run_prettier_on_changed.ts b/src/dev/run_prettier_on_changed.ts new file mode 100644 index 0000000000000..deca4fa1be3ce --- /dev/null +++ b/src/dev/run_prettier_on_changed.ts @@ -0,0 +1,87 @@ +/* + * 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. + */ + +import execa from 'execa'; +// @ts-ignore +import SimpleGit from 'simple-git'; +import { run } from '@kbn/dev-utils'; +import dedent from 'dedent'; +import Util from 'util'; + +import pkg from '../../package.json'; +import { REPO_ROOT } from './constants'; +import { File } from './file'; +import * as Eslint from './eslint'; + +run(async function getChangedFiles({ log }) { + const simpleGit = new SimpleGit(REPO_ROOT); + + const getStatus = Util.promisify(simpleGit.status.bind(simpleGit)); + const gitStatus = await getStatus(); + + if (gitStatus.files.length > 0) { + throw new Error( + dedent(`You should run prettier formatter on a clean branch. + Found not committed changes to: + ${gitStatus.files.map((f: { path: string }) => f.path).join('\n')}`) + ); + } + + const revParse = Util.promisify(simpleGit.revparse.bind(simpleGit)); + const currentBranch = await revParse(['--abbrev-ref', 'HEAD']); + const headBranch = pkg.branch; + + const diff = Util.promisify(simpleGit.diff.bind(simpleGit)); + + const changedFileStatuses: string = await diff([ + '--name-status', + `${headBranch}...${currentBranch}`, + ]); + + const changedFiles = changedFileStatuses + .split('\n') + // Ignore blank lines + .filter(line => line.trim().length > 0) + // git diff --name-status outputs lines with two OR three parts + // separated by a tab character + .map(line => line.trim().split('\t')) + .map(([status, ...paths]) => { + // ignore deleted files + if (status === 'D') { + return undefined; + } + + // the status is always in the first column + // .. If the file is edited the line will only have two columns + // .. If the file is renamed it will have three columns + // .. In any case, the last column is the CURRENT path to the file + return new File(paths[paths.length - 1]); + }) + .filter((file): file is File => Boolean(file)); + + const pathsToLint = Eslint.pickFilesToLint(log, changedFiles).map(f => f.getAbsolutePath()); + + if (pathsToLint.length > 0) { + log.debug('[prettier] run on %j files: ', pathsToLint.length, pathsToLint); + } + + while (pathsToLint.length > 0) { + await execa('npx', ['prettier@2.0.4', '--write', ...pathsToLint.splice(0, 100)]); + } +}); diff --git a/src/legacy/core_plugins/input_control_vis/index.ts b/src/legacy/core_plugins/input_control_vis/index.ts deleted file mode 100644 index 0529aa24dffd7..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/index.ts +++ /dev/null @@ -1,44 +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. - */ - -import { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const inputControlVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'input_control_vis', - require: ['kibana', 'elasticsearch', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default inputControlVisPluginInitializer; diff --git a/src/legacy/core_plugins/input_control_vis/package.json b/src/legacy/core_plugins/input_control_vis/package.json deleted file mode 100644 index 0d52be412f2fd..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "input_control_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts deleted file mode 100644 index e6426e5a4c69d..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts +++ /dev/null @@ -1,145 +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. - */ - -import { listControlFactory, ListControl } from './list_control_factory'; -import { ControlParams, CONTROL_TYPES } from '../editor_utils'; -import { getDepsMock, getSearchSourceMock } from '../test_utils'; - -const MockSearchSource = getSearchSourceMock(); -const deps = getDepsMock(); - -jest.doMock('./create_search_source.ts', () => ({ - createSearchSource: MockSearchSource, -})); - -describe('hasValue', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - const useTimeFilter = false; - - let listControl: ListControl; - beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); - }); - - test('should be false when control has no value', () => { - expect(listControl.hasValue()).toBe(false); - }); - - test('should be true when control has value', () => { - listControl.set([{ value: 'selected option', label: 'selection option' }]); - expect(listControl.hasValue()).toBe(true); - }); - - test('should be true when control has value that is the string "false"', () => { - listControl.set([{ value: 'false', label: 'selection option' }]); - expect(listControl.hasValue()).toBe(true); - }); -}); - -describe('fetch', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - const useTimeFilter = false; - - let listControl: ListControl; - beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); - }); - - test('should pass in timeout parameters from injected vars', async () => { - await listControl.fetch(); - expect(MockSearchSource).toHaveBeenCalledWith({ - timeout: `1000ms`, - terminate_after: 100000, - }); - }); - - test('should set selectOptions to results of terms aggregation', async () => { - await listControl.fetch(); - expect(listControl.selectOptions).toEqual([ - 'Zurich Airport', - 'Xi an Xianyang International Airport', - ]); - }); -}); - -describe('fetch with ancestors', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - const useTimeFilter = false; - - let listControl: ListControl; - let parentControl; - beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); - - const parentControlParams: ControlParams = { - id: 'parent', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - parentControl = await listControlFactory( - parentControlParams, - useTimeFilter, - MockSearchSource, - deps - ); - parentControl.clear(); - listControl.setAncestors([parentControl]); - }); - - describe('ancestor does not have value', () => { - test('should disable control', async () => { - await listControl.fetch(); - expect(listControl.isEnabled()).toBe(false); - }); - - test('should reset lastAncestorValues', async () => { - listControl.lastAncestorValues = 'last ancestor value'; - await listControl.fetch(); - expect(listControl.lastAncestorValues).toBeUndefined(); - }); - }); -}); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts deleted file mode 100644 index 32df9de8ac983..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts +++ /dev/null @@ -1,91 +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. - */ - -import { rangeControlFactory } from './range_control_factory'; -import { ControlParams, CONTROL_TYPES } from '../editor_utils'; -import { getDepsMock, getSearchSourceMock } from '../test_utils'; - -const deps = getDepsMock(); - -describe('fetch', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myNumberField', - options: {}, - type: CONTROL_TYPES.RANGE, - label: 'test', - indexPattern: {} as any, - parent: {} as any, - }; - const useTimeFilter = false; - - test('should set min and max from aggregation results', async () => { - const esSearchResponse = { - aggregations: { - maxAgg: { value: 100 }, - minAgg: { value: 10 }, - }, - }; - const rangeControl = await rangeControlFactory( - controlParams, - useTimeFilter, - getSearchSourceMock(esSearchResponse), - deps - ); - await rangeControl.fetch(); - - expect(rangeControl.isEnabled()).toBe(true); - expect(rangeControl.min).toBe(10); - expect(rangeControl.max).toBe(100); - }); - - test('should disable control when there are 0 hits', async () => { - // ES response when the query does not match any documents - const esSearchResponse = { - aggregations: { - maxAgg: { value: null }, - minAgg: { value: null }, - }, - }; - const rangeControl = await rangeControlFactory( - controlParams, - useTimeFilter, - getSearchSourceMock(esSearchResponse), - deps - ); - await rangeControl.fetch(); - - expect(rangeControl.isEnabled()).toBe(false); - }); - - test('should disable control when response is empty', async () => { - // ES response for dashboardonly user who does not have read permissions on index is 200 (which is weird) - // and there is not aggregations key - const esSearchResponse = {}; - const rangeControl = await rangeControlFactory( - controlParams, - useTimeFilter, - getSearchSourceMock(esSearchResponse), - deps - ); - await rangeControl.fetch(); - - expect(rangeControl.isEnabled()).toBe(false); - }); -}); diff --git a/src/legacy/core_plugins/input_control_vis/public/index.scss b/src/legacy/core_plugins/input_control_vis/public/index.scss deleted file mode 100644 index ac4692494b923..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/index.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "icv" to avoid conflicts. -// Examples -// icvChart -// icvChart__legend -// icvChart__legend--small -// icvChart__legend-isLoading - -@import './components/editor/index'; -@import './components/vis/index'; diff --git a/src/legacy/core_plugins/input_control_vis/public/index.ts b/src/legacy/core_plugins/input_control_vis/public/index.ts deleted file mode 100644 index e14c2cc4b69b6..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/index.ts +++ /dev/null @@ -1,25 +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. - */ - -import { PluginInitializerContext } from '../../../../core/public'; -import { InputControlVisPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy.ts b/src/legacy/core_plugins/input_control_vis/public/legacy.ts deleted file mode 100644 index 67299068819e8..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/legacy.ts +++ /dev/null @@ -1,45 +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. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from '.'; - -import { - InputControlVisPluginSetupDependencies, - InputControlVisPluginStartDependencies, -} from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - data: npSetup.plugins.data, - visualizations: npSetup.plugins.visualizations, -}; - -const startPlugins: Readonly = { - expressions: npStart.plugins.expressions, - data: npStart.plugins.data, - visualizations: npStart.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts deleted file mode 100644 index 8c58ac2386da4..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.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. - */ - -import { Class } from '@kbn/utility-types'; -import { SearchSource as SearchSourceClass, ISearchSource } from '../../../../plugins/data/public'; - -export { SearchSourceFields } from '../../../../plugins/data/public'; - -export type SearchSource = Class; -export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/legacy/core_plugins/input_control_vis/public/plugin.ts deleted file mode 100644 index b743468065430..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/plugin.ts +++ /dev/null @@ -1,73 +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. - */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; - -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; -import { createInputControlVisFn } from './input_control_fn'; -import { createInputControlVisTypeDefinition } from './input_control_vis_type'; - -type InputControlVisCoreSetup = CoreSetup; - -export interface InputControlVisDependencies { - core: InputControlVisCoreSetup; - data: DataPublicPluginSetup; -} - -/** @internal */ -export interface InputControlVisPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - data: DataPublicPluginSetup; -} - -/** @internal */ -export interface InputControlVisPluginStartDependencies { - expressions: ReturnType; - visualizations: VisualizationsStart; - data: DataPublicPluginStart; -} - -/** @internal */ -export class InputControlVisPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - - public async setup( - core: InputControlVisCoreSetup, - { expressions, visualizations, data }: InputControlVisPluginSetupDependencies - ) { - const visualizationDependencies: Readonly = { - core, - data, - }; - - expressions.registerFunction(createInputControlVisFn); - visualizations.createBaseVisualization( - createInputControlVisTypeDefinition(visualizationDependencies) - ); - } - - public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) { - // nothing to do here - } -} diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 989583742acd0..8465d71e1e998 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -21,7 +21,6 @@ import Fs from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; -import { migrations } from './migrations'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import mappings from './mappings.json'; @@ -120,23 +119,6 @@ export default function(kibana) { ], savedObjectsManagement: { - dashboard: { - icon: 'dashboardApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'dashboard.show', - }; - }, - }, url: { defaultSearchField: 'url', isImportableAndExportable: true, @@ -147,9 +129,6 @@ export default function(kibana) { }, savedObjectSchemas: { - 'sample-data-telemetry': { - isNamespaceAgnostic: true, - }, 'kql-telemetry': { isNamespaceAgnostic: true, }, @@ -177,8 +156,6 @@ export default function(kibana) { mappings, uiSettingDefaults: getUiSettingDefaults(), - - migrations, }, uiCapabilities: async function() { diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json index af3f79588552b..febdf2cc3d649 100644 --- a/src/legacy/core_plugins/kibana/mappings.json +++ b/src/legacy/core_plugins/kibana/mappings.json @@ -1,58 +1,4 @@ { - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, "url": { "properties": { "accessCount": { @@ -91,15 +37,5 @@ "type": "long" } } - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } } } diff --git a/src/legacy/core_plugins/kibana/migrations/index.ts b/src/legacy/core_plugins/kibana/migrations/index.ts deleted file mode 100644 index 68c843d2343c8..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/index.ts +++ /dev/null @@ -1,21 +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. - */ - -// @ts-ignore -export { migrations } from './migrations'; diff --git a/src/legacy/core_plugins/kibana/migrations/is_doc.ts b/src/legacy/core_plugins/kibana/migrations/is_doc.ts deleted file mode 100644 index cc50dfa3b2d26..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/is_doc.ts +++ /dev/null @@ -1,31 +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. - */ - -import { Doc } from './types'; - -export function isDoc(doc: { [key: string]: unknown } | Doc): doc is Doc { - return ( - typeof doc.id === 'string' && - typeof doc.type === 'string' && - doc.attributes !== null && - typeof doc.attributes === 'object' && - doc.references !== null && - typeof doc.references === 'object' - ); -} diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js deleted file mode 100644 index 029dbde555a4b..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/migrations.js +++ /dev/null @@ -1,106 +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. - */ - -import { get } from 'lodash'; -import { - migrateMatchAllQuery, - migrations730 as dashboardMigrations730, -} from '../public/dashboard/migrations'; - -function migrateIndexPattern(doc) { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); - if (typeof searchSourceJSON !== 'string') { - return; - } - let searchSource; - try { - searchSource = JSON.parse(searchSourceJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return; - } - if (searchSource.index) { - searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - doc.references.push({ - name: searchSource.indexRefName, - type: 'index-pattern', - id: searchSource.index, - }); - delete searchSource.index; - } - if (searchSource.filter) { - searchSource.filter.forEach((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return; - } - filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - doc.references.push({ - name: filterRow.meta.indexRefName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - delete filterRow.meta.index; - }); - } - doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); -} - -export const migrations = { - dashboard: { - '6.7.2': migrateMatchAllQuery, - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - - // Migrate index pattern - migrateIndexPattern(doc); - // Migrate panels - const panelsJSON = get(doc, 'attributes.panelsJSON'); - if (typeof panelsJSON !== 'string') { - return doc; - } - let panels; - try { - panels = JSON.parse(panelsJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc; - } - if (!Array.isArray(panels)) { - return doc; - } - panels.forEach((panel, i) => { - if (!panel.type || !panel.id) { - return; - } - panel.panelRefName = `panel_${i}`; - doc.references.push({ - name: `panel_${i}`, - type: panel.type, - id: panel.id, - }); - delete panel.type; - delete panel.id; - }); - doc.attributes.panelsJSON = JSON.stringify(panels); - return doc; - }, - '7.3.0': dashboardMigrations730, - }, -}; diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.test.js b/src/legacy/core_plugins/kibana/migrations/migrations.test.js deleted file mode 100644 index b02081128c858..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/migrations.test.js +++ /dev/null @@ -1,447 +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. - */ - -import { migrations } from './migrations'; - -describe('dashboard', () => { - describe('7.0.0', () => { - const migration = migrations.dashboard['7.0.0']; - - test('skips error on empty object', () => { - expect(migration({})).toMatchInlineSnapshot(` -Object { - "references": Array [], -} -`); - }); - - test('skips errors when searchSourceJSON is null', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: null, - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips errors when searchSourceJSON is undefined', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: undefined, - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when searchSourceJSON is not a string', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: 123, - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when searchSourceJSON is invalid json', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: '{abc123}', - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true }), - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('extracts "index" attribute from doc', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('extracts index patterns from filter', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - bar: true, - filter: [ - { - meta: { - foo: true, - index: 'my-index', - }, - }, - ], - }), - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when panelsJSON is not a string', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: 123, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": 123, - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips error when panelsJSON is not valid JSON', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '{123abc}', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "{123abc}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips panelsJSON when its not an array', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '{}', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "{}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips error when a panel is missing "type" attribute', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '[{"id":"123"}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"id\\":\\"123\\"}]", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips error when a panel is missing "id" attribute', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '[{"type":"visualization"}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('extract panel references from doc', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/migrations/types.ts b/src/legacy/core_plugins/kibana/migrations/types.ts deleted file mode 100644 index 839f753670b20..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/types.ts +++ /dev/null @@ -1,40 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectReference } from '../../../../core/server'; - -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} - -export interface Doc { - references: SavedObjectReference[]; - attributes: Attributes; - id: string; - type: string; -} - -export interface DocPre700 { - attributes: Attributes; - id: string; - type: string; -} diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js new file mode 100644 index 0000000000000..b212ecf578dd1 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js @@ -0,0 +1,490 @@ +/* + * 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. + */ + +import $ from 'jquery'; +import moment from 'moment'; +import ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; +import './legacy'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { npStart } from 'ui/new_platform'; +import { round } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInnerAngular } from '../../../../../../plugins/vis_type_table/public/get_inner_angular'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { initTableVisLegacyModule } from '../../../../../../plugins/vis_type_table/public/table_vis_legacy_module'; +import { tabifiedData } from './tabified_data'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { configureAppAngularModule } from '../../../../../../plugins/kibana_legacy/public/angular'; + +describe('Table Vis - AggTable Directive', function() { + let $rootScope; + let $compile; + let settings; + + const initLocalAngular = () => { + const tableVisModule = getInnerAngular('kibana/table_vis', npStart.core); + configureAppAngularModule(tableVisModule, npStart.core, true); + initTableVisLegacyModule(tableVisModule); + }; + + beforeEach(initLocalAngular); + + beforeEach(ngMock.module('kibana/table_vis')); + beforeEach( + ngMock.inject(function($injector, config) { + settings = config; + + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); + }) + ); + + let $scope; + beforeEach(function() { + $scope = $rootScope.$new(); + }); + afterEach(function() { + $scope.$destroy(); + }); + + it('renders a simple response properly', function() { + $scope.dimensions = { + metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], + buckets: [], + }; + $scope.table = tabifiedData.metricOnly.tables[0]; + + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + expect($el.find('tbody').length).to.be(1); + expect($el.find('td').length).to.be(1); + expect($el.find('td').text()).to.eql('1,000'); + }); + + it('renders nothing if the table is empty', function() { + $scope.dimensions = {}; + $scope.table = null; + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + expect($el.find('tbody').length).to.be(0); + }); + + it('renders a complex response properly', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], + }; + $scope.table = tabifiedData.threeTermBuckets.tables[0]; + const $el = $(''); + $compile($el)($scope); + $scope.$digest(); + + expect($el.find('tbody').length).to.be(1); + + const $rows = $el.find('tbody tr'); + expect($rows.length).to.be.greaterThan(0); + + function validBytes(str) { + const num = str.replace(/,/g, ''); + if (num !== '-') { + expect(num).to.match(/^\d+$/); + } + } + + $rows.each(function() { + // 6 cells in every row + const $cells = $(this).find('td'); + expect($cells.length).to.be(6); + + const txts = $cells.map(function() { + return $(this) + .text() + .trim(); + }); + + // two character country code + expect(txts[0]).to.match(/^(png|jpg|gif|html|css)$/); + validBytes(txts[1]); + + // country + expect(txts[2]).to.match(/^\w\w$/); + validBytes(txts[3]); + + // os + expect(txts[4]).to.match(/^(win|mac|linux)$/); + validBytes(txts[5]); + }); + }); + + describe('renders totals row', function() { + async function totalsRowTest(totalFunc, expected) { + function setDefaultTimezone() { + moment.tz.setDefault(settings.get('dateFormat:tz')); + } + + const off = $scope.$on('change:config.dateFormat:tz', setDefaultTimezone); + const oldTimezoneSetting = settings.get('dateFormat:tz'); + settings.set('dateFormat:tz', 'UTC'); + + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, + ], + metrics: [ + { accessor: 2, format: { id: 'number' } }, + { accessor: 3, format: { id: 'date' } }, + { accessor: 4, format: { id: 'number' } }, + { accessor: 5, format: { id: 'number' } }, + ], + }; + $scope.table = + tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; + $scope.showTotal = true; + $scope.totalFunc = totalFunc; + const $el = $(``); + $compile($el)($scope); + $scope.$digest(); + + expect($el.find('tfoot').length).to.be(1); + + const $rows = $el.find('tfoot tr'); + expect($rows.length).to.be(1); + + const $cells = $($rows[0]).find('th'); + expect($cells.length).to.be(6); + + for (let i = 0; i < 6; i++) { + expect( + $($cells[i]) + .text() + .trim() + ).to.be(expected[i]); + } + settings.set('dateFormat:tz', oldTimezoneSetting); + off(); + } + it('as count', async function() { + await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); + }); + it('as min', async function() { + await totalsRowTest('min', [ + '', + '2014-09-28', + '9,283', + 'Sep 28, 2014 @ 00:00:00.000', + '1', + '11', + ]); + }); + it('as max', async function() { + await totalsRowTest('max', [ + '', + '2014-10-03', + '220,943', + 'Oct 3, 2014 @ 00:00:00.000', + '239', + '837', + ]); + }); + it('as avg', async function() { + await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); + }); + it('as sum', async function() { + await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); + }); + }); + + describe('aggTable.toCsv()', function() { + it('escapes rows and columns properly', function() { + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = { + columns: [ + { id: 'a', name: 'one' }, + { id: 'b', name: 'two' }, + { id: 'c', name: 'with double-quotes(")' }, + ], + rows: [{ a: 1, b: 2, c: '"foobar"' }], + }; + + expect(aggTable.toCsv()).to.be( + 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' + ); + }); + + it('exports rows and columns properly', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], + }; + $scope.table = tabifiedData.threeTermBuckets.tables[0]; + + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = $scope.table; + + const raw = aggTable.toCsv(false); + expect(raw).to.be( + '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + + '\r\n' + + 'png,IT,win,412032,9299,0' + + '\r\n' + + 'png,IT,mac,412032,9299,9299' + + '\r\n' + + 'png,US,linux,412032,8293,3992' + + '\r\n' + + 'png,US,mac,412032,8293,3029' + + '\r\n' + + 'css,MX,win,412032,9299,4992' + + '\r\n' + + 'css,MX,mac,412032,9299,5892' + + '\r\n' + + 'css,US,linux,412032,8293,3992' + + '\r\n' + + 'css,US,mac,412032,8293,3029' + + '\r\n' + + 'html,CN,win,412032,9299,4992' + + '\r\n' + + 'html,CN,mac,412032,9299,5892' + + '\r\n' + + 'html,FR,win,412032,8293,3992' + + '\r\n' + + 'html,FR,mac,412032,8293,3029' + + '\r\n' + ); + }); + + it('exports formatted rows and columns properly', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], + }; + $scope.table = tabifiedData.threeTermBuckets.tables[0]; + + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = $scope.table; + + // Create our own converter since the ones we use for tests don't actually transform the provided value + $tableScope.formattedColumns[0].formatter.convert = v => `${v}_formatted`; + + const formatted = aggTable.toCsv(true); + expect(formatted).to.be( + '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + + '\r\n' + + '"png_formatted",IT,win,412032,9299,0' + + '\r\n' + + '"png_formatted",IT,mac,412032,9299,9299' + + '\r\n' + + '"png_formatted",US,linux,412032,8293,3992' + + '\r\n' + + '"png_formatted",US,mac,412032,8293,3029' + + '\r\n' + + '"css_formatted",MX,win,412032,9299,4992' + + '\r\n' + + '"css_formatted",MX,mac,412032,9299,5892' + + '\r\n' + + '"css_formatted",US,linux,412032,8293,3992' + + '\r\n' + + '"css_formatted",US,mac,412032,8293,3029' + + '\r\n' + + '"html_formatted",CN,win,412032,9299,4992' + + '\r\n' + + '"html_formatted",CN,mac,412032,9299,5892' + + '\r\n' + + '"html_formatted",FR,win,412032,8293,3992' + + '\r\n' + + '"html_formatted",FR,mac,412032,8293,3029' + + '\r\n' + ); + }); + }); + + it('renders percentage columns', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, + ], + metrics: [ + { accessor: 2, format: { id: 'number' } }, + { accessor: 3, format: { id: 'date' } }, + { accessor: 4, format: { id: 'number' } }, + { accessor: 5, format: { id: 'number' } }, + ], + }; + $scope.table = + tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; + $scope.percentageCol = 'Average bytes'; + + const $el = $(``); + + $compile($el)($scope); + $scope.$digest(); + + const $headings = $el.find('th'); + expect($headings.length).to.be(7); + expect( + $headings + .eq(3) + .text() + .trim() + ).to.be('Average bytes percentages'); + + const countColId = $scope.table.columns.find(col => col.name === $scope.percentageCol).id; + const counts = $scope.table.rows.map(row => row[countColId]); + const total = counts.reduce((sum, curr) => sum + curr, 0); + const $percentageColValues = $el.find('tbody tr').map((i, el) => + $(el) + .find('td') + .eq(3) + .text() + ); + + $percentageColValues.each((i, value) => { + const percentage = `${round((counts[i] / total) * 100, 1)}%`; + expect(value).to.be(percentage); + }); + }); + + describe('aggTable.exportAsCsv()', function() { + let origBlob; + function FakeBlob(slices, opts) { + this.slices = slices; + this.opts = opts; + } + + beforeEach(function() { + origBlob = window.Blob; + window.Blob = FakeBlob; + }); + + afterEach(function() { + window.Blob = origBlob; + }); + + it('calls _saveAs properly', function() { + const $el = $compile('')($scope); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + + const saveAs = sinon.stub(aggTable, '_saveAs'); + $tableScope.table = { + columns: [ + { id: 'a', name: 'one' }, + { id: 'b', name: 'two' }, + { id: 'c', name: 'with double-quotes(")' }, + ], + rows: [{ a: 1, b: 2, c: '"foobar"' }], + }; + + aggTable.csv.filename = 'somefilename.csv'; + aggTable.exportAsCsv(); + + expect(saveAs.callCount).to.be(1); + const call = saveAs.getCall(0); + expect(call.args[0]).to.be.a(FakeBlob); + expect(call.args[0].slices).to.eql([ + 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', + ]); + expect(call.args[0].opts).to.eql({ + type: 'text/plain;charset=utf-8', + }); + expect(call.args[1]).to.be('somefilename.csv'); + }); + + it('should use the export-title attribute', function() { + const expected = 'export file name'; + const $el = $compile( + `` + )($scope); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = { + columns: [], + rows: [], + }; + $tableScope.exportTitle = expected; + $scope.$digest(); + + expect(aggTable.csv.filename).to.equal(`${expected}.csv`); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js similarity index 81% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js index 40a0993ccb017..3cd7de393d66a 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js @@ -20,17 +20,24 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { npStart } from '../../legacy_imports'; -import { getAngularModule } from '../../get_inner_angular'; -import { initTableVisLegacyModule } from '../../table_vis_legacy_module'; +import './legacy'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInnerAngular } from '../../../../../../plugins/vis_type_table/public/get_inner_angular'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { initTableVisLegacyModule } from '../../../../../../plugins/vis_type_table/public/table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { npStart } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { configureAppAngularModule } from '../../../../../../plugins/kibana_legacy/public/angular'; describe('Table Vis - AggTableGroup Directive', function() { let $rootScope; let $compile; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', npStart.core); + const tableVisModule = getInnerAngular('kibana/table_vis', npStart.core); + configureAppAngularModule(tableVisModule, npStart.core, true); initTableVisLegacyModule(tableVisModule); }; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts new file mode 100644 index 0000000000000..c6467a5beae68 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ +import { PluginInitializerContext } from 'kibana/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { npStart, npSetup } from 'ui/new_platform'; +import { + TableVisPlugin, + TablePluginSetupDependencies, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/vis_type_table/public/plugin'; + +const plugins: Readonly = { + expressions: npSetup.plugins.expressions, + visualizations: npSetup.plugins.visualizations, +}; + +const pluginInstance = new TableVisPlugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, plugins); +export const start = pluginInstance.start(npStart.core, { + data: npStart.plugins.data, +}); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/tabified_data.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/tabified_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/tabified_data.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/tabified_data.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js similarity index 98% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js index 152efe5667f18..8f08f6a1f37e6 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import _ from 'lodash'; import d3 from 'd3'; -import { TagCloud } from '../tag_cloud'; import { fromNode, delay } from 'bluebird'; import { ImageComparator } from 'test_utils/image_comparator'; import simpleloadPng from './simpleload.png'; @@ -29,6 +28,9 @@ import simpleloadPng from './simpleload.png'; // Replace with mock when converting to jest tests // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TagCloud } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud'; describe('tag cloud tests', function() { const minValue = 1; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js similarity index 89% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js index 9e611861417cd..040ee18916fa2 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js @@ -20,7 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { ImageComparator } from 'test_utils/image_comparator'; -import { createTagCloudVisualization } from '../tag_cloud_visualization'; import basicdrawPng from './basicdraw.png'; import afterresizePng from './afterresize.png'; import afterparamChange from './afterparamchange.png'; @@ -32,7 +31,14 @@ import { ExprVis } from '../../../../../../plugins/visualizations/public/express import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; -import { createTagCloudVisTypeDefinition } from '../../tag_cloud_type'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createTagCloudVisTypeDefinition } from '../../../../../../plugins/vis_type_tagcloud/public/tag_cloud_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createTagCloudVisualization } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud_visualization'; +import { npStart } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setFormatService } from '../../../../../../plugins/vis_type_tagcloud/public/services'; const THRESHOLD = 0.65; const PIXEL_DIFF = 64; @@ -66,6 +72,8 @@ describe('TagCloudVisualizationTest', function() { }, }); + before(() => setFormatService(npStart.plugins.data.fieldFormats)); + beforeEach(ngMock.module('kibana')); describe('TagCloudVisualization - basics', function() { diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js similarity index 86% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 6412d8a569b2a..21b7ea7dbf4c3 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -21,7 +21,9 @@ import Bluebird from 'bluebird'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import $ from 'jquery'; -import { createVegaVisualization } from '../vega_visualization'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaVisualization } from '../../../../../../plugins/vis_type_vega/public/vega_visualization'; import { ImageComparator } from 'test_utils/image_comparator'; import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson'; @@ -35,24 +37,34 @@ import vegaTooltipGraph from '!!raw-loader!./vega_tooltip_test.hjson'; import vegaMapGraph from '!!raw-loader!./vega_map_test.hjson'; import vegaMapImage256 from './vega_map_image_256.png'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { VegaParser } from '../../../../../../plugins/vis_type_vega/public/data_model/vega_parser'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SearchCache } from '../../../../../../plugins/vis_type_vega/public/data_model/search_cache'; -import { VegaParser } from '../data_model/vega_parser'; -import { SearchCache } from '../data_model/search_cache'; - -import { createVegaTypeDefinition } from '../vega_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaTypeDefinition } from '../../../../../../plugins/vis_type_vega/public/vega_type'; // TODO This is an integration test and thus requires a running platform. When moving to the new platform, // this test has to be migrated to the newly created integration test environment. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { npStart } from 'ui/new_platform'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_types/base_vis_type'; +import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; -import { setInjectedVars } from '../services'; +import { ExprVis } from '../../../../../../plugins/visualizations/public/expressions/vis'; + +import { + setInjectedVars, + setData, + setSavedObjects, + setNotifications, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/vis_type_vega/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +import { setInjectedVarFunc } from '../../../../../../plugins/maps_legacy/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; +import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -70,6 +82,9 @@ describe('VegaVisualizations', () => { enableExternalUrls: true, esShardTimeout: 10000, }); + setData(npStart.plugins.data); + setSavedObjects(npStart.core.savedObjects); + setNotifications(npStart.core.notifications); beforeEach(ngMock.module('kibana')); beforeEach( @@ -111,9 +126,6 @@ describe('VegaVisualizations', () => { timefilter: {}, }, }, - __LEGACY: { - esClient: npStart.plugins.data.search.__LEGACY.esClient, - }, }, }, }; diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts deleted file mode 100644 index f333ce97d120f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts +++ /dev/null @@ -1,21 +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. - */ - -export { migrations730 } from './migrations_730'; -export { migrateMatchAllQuery } from './migrate_match_all_query'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts deleted file mode 100644 index d8f8882a218dd..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts +++ /dev/null @@ -1,35 +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. - */ - -import { DashboardDoc730ToLatest } from '../../../../../../plugins/dashboard/public'; -import { isDoc } from '../../../migrations/is_doc'; - -export function isDashboardDoc( - doc: { [key: string]: unknown } | DashboardDoc730ToLatest -): doc is DashboardDoc730ToLatest { - if (!isDoc(doc)) { - return false; - } - - if (typeof (doc as DashboardDoc730ToLatest).attributes.panelsJSON !== 'string') { - return false; - } - - return true; -} diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index a3a99a0ded523..b987129a9a7ed 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { createHashHistory, History } from 'history'; +import { History } from 'history'; import { Capabilities, @@ -42,6 +42,7 @@ import { DocViewerComponent, SavedSearch, } from '../../../../../plugins/discover/public'; +import { SavedObjectKibanaServices } from '../../../../../plugins/saved_objects/public'; export interface DiscoverServices { addBasePath: (path: string) => string; @@ -51,7 +52,7 @@ export interface DiscoverServices { data: DataPublicPluginStart; docLinks: DocLinksStart; DocViewer: DocViewerComponent; - history: History; + history: () => History; theme: ChartsPluginStart['theme']; filterManager: FilterManager; indexPatterns: IndexPatternsContract; @@ -65,11 +66,13 @@ export interface DiscoverServices { uiSettings: IUiSettingsClient; visualizations: VisualizationsStart; } + export async function buildServices( core: CoreStart, - plugins: DiscoverStartPlugins + plugins: DiscoverStartPlugins, + getHistory: () => History ): Promise { - const services = { + const services: SavedObjectKibanaServices = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, search: plugins.data.search, @@ -77,6 +80,7 @@ export async function buildServices( overlays: core.overlays, }; const savedObjectService = createSavedSearchesLoader(services); + return { addBasePath: core.http.basePath.prepend, capabilities: core.application.capabilities, @@ -85,11 +89,11 @@ export async function buildServices( data: plugins.data, docLinks: core.docLinks, DocViewer: plugins.discover.docViews.DocViewer, - history: createHashHistory(), theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, getSavedSearchById: async (id: string) => savedObjectService.get(id), getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), + history: getHistory, indexPatterns: plugins.data.indexPatterns, inspector: plugins.inspector, // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 0a81ca0222b0a..77664e87a3279 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { createHashHistory } from 'history'; import { DiscoverServices } from './build_services'; import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; import { search } from '../../../../../plugins/data/public'; @@ -52,12 +53,13 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; }>('urlTracker'); +/** + * Makes sure discover and context are using one instance of history + */ +export const getHistory = _.once(() => createHashHistory()); + export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; -export { - unhashUrl, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../../../plugins/kibana_utils/public'; +export { unhashUrl, redirectWhenMissing } from '../../../../../plugins/kibana_utils/public'; export { formatMsg, formatStack, @@ -71,7 +73,6 @@ export { IndexPattern, indexPatterns, IFieldType, - SearchSource, ISearchSource, EsQuerySortValue, SortDirection, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js index 5b03b313e4e3e..032ec7af09a30 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js @@ -81,6 +81,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { defaultStepSize: getServices().uiSettings.get('context:defaultSize'), timeFieldName: indexPattern.timeFieldName, storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), + history: getServices().history(), }); this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js index f6ed570be2c37..acd609df203e3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js @@ -19,7 +19,6 @@ import sinon from 'sinon'; import moment from 'moment'; -import { SearchSource } from '../../../../../../../../../plugins/data/public'; export function createIndexPatternsStub() { return { @@ -46,17 +45,15 @@ export function createSearchSourceStub(hits, timeField) { }), }; - searchSourceStub.setParent = sinon - .stub(SearchSource.prototype, 'setParent') - .returns(searchSourceStub); - searchSourceStub.setField = sinon - .stub(SearchSource.prototype, 'setField') - .returns(searchSourceStub); - searchSourceStub.getField = sinon.stub(SearchSource.prototype, 'getField').callsFake(key => { + searchSourceStub.setParent = sinon.spy(() => searchSourceStub); + searchSourceStub.setField = sinon.spy(() => searchSourceStub); + + searchSourceStub.getField = sinon.spy(key => { const previousSetCall = searchSourceStub.setField.withArgs(key).lastCall; return previousSetCall ? previousSetCall.args[1] : null; }); - searchSourceStub.fetch = sinon.stub(SearchSource.prototype, 'fetch').callsFake(() => + + searchSourceStub.fetch = sinon.spy(() => Promise.resolve({ hits: { hits: searchSourceStub._stubHits, @@ -65,13 +62,6 @@ export function createSearchSourceStub(hits, timeField) { }) ); - searchSourceStub._restore = () => { - searchSourceStub.setParent.restore(); - searchSourceStub.setField.restore(); - searchSourceStub.getField.restore(); - searchSourceStub.fetch.restore(); - }; - return searchSourceStub; } @@ -81,8 +71,7 @@ export function createSearchSourceStub(hits, timeField) { export function createContextSearchSourceStub(hits, timeField = '@timestamp') { const searchSourceStub = createSearchSourceStub(hits, timeField); - searchSourceStub.fetch.restore(); - searchSourceStub.fetch = sinon.stub(SearchSource.prototype, 'fetch').callsFake(() => { + searchSourceStub.fetch = sinon.spy(() => { const timeField = searchSourceStub._stubTimeField; const lastQuery = searchSourceStub.setField.withArgs('query').lastCall.args[1]; const timeRange = lastQuery.query.constant_score.filter.range[timeField]; @@ -99,6 +88,7 @@ export function createContextSearchSourceStub(hits, timeField = '@timestamp') { moment(hit[timeField]).isSameOrBefore(timeRange.lte) ) .sort(sortFunction); + return Promise.resolve({ hits: { hits: filteredHits, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js index 0bc2cbacc1eee..757e74589555a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js @@ -31,10 +31,6 @@ describe('context app', function() { fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); }); - afterEach(() => { - searchSourceStub._restore(); - }); - it('should use the `fetch` method of the SearchSource', function() { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ { '@timestamp': 'desc' }, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js index d6e91e57b22a8..ebd4af536aabd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js @@ -21,6 +21,7 @@ import moment from 'moment'; import * as _ from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { fetchContextProvider } from './context'; +import { setServices } from '../../../../kibana_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -31,10 +32,21 @@ const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); describe('context app', function() { describe('function fetchPredecessors', function() { let fetchPredecessors; - let searchSourceStub; + let mockSearchSource; beforeEach(() => { - searchSourceStub = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + mockSearchSource = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + + setServices({ + data: { + search: { + searchSource: { + create: jest.fn().mockImplementation(() => mockSearchSource), + }, + }, + }, + }); + fetchPredecessors = ( indexPatternId, timeField, @@ -65,17 +77,13 @@ describe('context app', function() { }; }); - afterEach(() => { - searchSourceStub._restore(); - }); - it('should perform exactly one query when enough hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3000 + 2), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 + 1), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 2000), - searchSourceStub._createStubHit(MS_PER_DAY * 1000), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 2), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 2000), + mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; return fetchPredecessors( @@ -89,18 +97,18 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(searchSourceStub.fetch.calledOnce).toBe(true); - expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 3)); + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); }); }); it('should perform multiple queries with the last being unrestricted when too few hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3010), - searchSourceStub._createStubHit(MS_PER_DAY * 3002), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 2998), - searchSourceStub._createStubHit(MS_PER_DAY * 2990), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3010), + mockSearchSource._createStubHit(MS_PER_DAY * 3002), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 2998), + mockSearchSource._createStubHit(MS_PER_DAY * 2990), ]; return fetchPredecessors( @@ -114,7 +122,7 @@ describe('context app', function() { 6, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -129,16 +137,16 @@ describe('context app', function() { expect(Object.keys(_.last(intervals))).toEqual(['format', 'gte']); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 3)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); }); }); it('should perform multiple queries until the expected hit count is returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 1700), - searchSourceStub._createStubHit(MS_PER_DAY * 1200), - searchSourceStub._createStubHit(MS_PER_DAY * 1100), - searchSourceStub._createStubHit(MS_PER_DAY * 1000), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 1700), + mockSearchSource._createStubHit(MS_PER_DAY * 1200), + mockSearchSource._createStubHit(MS_PER_DAY * 1100), + mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; return fetchPredecessors( @@ -152,7 +160,7 @@ describe('context app', function() { 3, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -163,7 +171,7 @@ describe('context app', function() { // should have stopped before reaching MS_PER_DAY * 1700 expect(moment(_.last(intervals).lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); }); @@ -195,7 +203,7 @@ describe('context app', function() { 3, [] ).then(() => { - const setParentSpy = searchSourceStub.setParent; + const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); expect(setParentSpy.called).toBe(true); }); @@ -214,7 +222,7 @@ describe('context app', function() { [] ).then(() => { expect( - searchSourceStub.setField.calledWith('sort', [{ '@timestamp': 'asc' }, { _doc: 'asc' }]) + mockSearchSource.setField.calledWith('sort', [{ '@timestamp': 'asc' }, { _doc: 'asc' }]) ).toBe(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js index cc2b6d31cb43b..452d0cc9fd1a0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js @@ -21,6 +21,7 @@ import moment from 'moment'; import * as _ from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; +import { setServices } from '../../../../kibana_services'; import { fetchContextProvider } from './context'; @@ -32,10 +33,20 @@ const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); describe('context app', function() { describe('function fetchSuccessors', function() { let fetchSuccessors; - let searchSourceStub; + let mockSearchSource; beforeEach(() => { - searchSourceStub = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + + setServices({ + data: { + search: { + searchSource: { + create: jest.fn().mockImplementation(() => mockSearchSource), + }, + }, + }, + }); fetchSuccessors = ( indexPatternId, @@ -67,17 +78,13 @@ describe('context app', function() { }; }); - afterEach(() => { - searchSourceStub._restore(); - }); - it('should perform exactly one query when enough hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 5000), - searchSourceStub._createStubHit(MS_PER_DAY * 4000), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 1), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 2), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 5000), + mockSearchSource._createStubHit(MS_PER_DAY * 4000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), ]; return fetchSuccessors( @@ -91,18 +98,18 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(searchSourceStub.fetch.calledOnce).toBe(true); - expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); }); it('should perform multiple queries with the last being unrestricted when too few hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3010), - searchSourceStub._createStubHit(MS_PER_DAY * 3002), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 2998), - searchSourceStub._createStubHit(MS_PER_DAY * 2990), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3010), + mockSearchSource._createStubHit(MS_PER_DAY * 3002), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 2998), + mockSearchSource._createStubHit(MS_PER_DAY * 2990), ]; return fetchSuccessors( @@ -116,7 +123,7 @@ describe('context app', function() { 6, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -131,18 +138,18 @@ describe('context app', function() { expect(Object.keys(_.last(intervals))).toEqual(['format', 'lte']); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); }); it('should perform multiple queries until the expected hit count is returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 1), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 2), - searchSourceStub._createStubHit(MS_PER_DAY * 2800), - searchSourceStub._createStubHit(MS_PER_DAY * 2200), - searchSourceStub._createStubHit(MS_PER_DAY * 1000), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), + mockSearchSource._createStubHit(MS_PER_DAY * 2800), + mockSearchSource._createStubHit(MS_PER_DAY * 2200), + mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; return fetchSuccessors( @@ -156,7 +163,7 @@ describe('context app', function() { 4, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -168,7 +175,7 @@ describe('context app', function() { expect(moment(_.last(intervals).gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 4)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 4)); }); }); @@ -200,7 +207,7 @@ describe('context app', function() { 3, [] ).then(() => { - const setParentSpy = searchSourceStub.setParent; + const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); expect(setParentSpy.called).toBe(true); }); @@ -219,7 +226,7 @@ describe('context app', function() { [] ).then(() => { expect( - searchSourceStub.setField.calledWith('sort', [{ '@timestamp': 'desc' }, { _doc: 'desc' }]) + mockSearchSource.setField.calledWith('sort', [{ '@timestamp': 'desc' }, { _doc: 'desc' }]) ).toBe(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts index 507f927c608e1..2760eec38755e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts @@ -27,8 +27,8 @@ import { Filter, IndexPatternsContract, IndexPattern, - SearchSource, } from '../../../../../../../../../plugins/data/public'; +import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { @@ -115,7 +115,10 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { } async function createSearchSource(indexPattern: IndexPattern, filters: Filter[]) { - return new SearchSource() + const { data } = getServices(); + + return data.search.searchSource + .create() .setParent(undefined) .setField('index', indexPattern) .setField('filter', filters); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js index 9efddc5275069..efc230d2cd4ae 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { getServices, SearchSource } from '../../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; @@ -29,8 +29,8 @@ import { FAILURE_REASONS, LOADING_STATUS } from './constants'; import { MarkdownSimple } from '../../../../../../../../../plugins/kibana_react/public'; export function QueryActionsProvider(Promise) { - const { filterManager, indexPatterns } = getServices(); - const fetchAnchor = fetchAnchorProvider(indexPatterns, new SearchSource()); + const { filterManager, indexPatterns, data } = getServices(); + const fetchAnchor = fetchAnchorProvider(indexPatterns, data.search.searchSource.create()); const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns); const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions( filterManager, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts index ed59143b163f6..b46995d44d826 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -17,7 +17,7 @@ * under the License. */ import _ from 'lodash'; -import { createBrowserHistory, History } from 'history'; +import { History } from 'history'; import { createStateContainer, createKbnUrlStateStorage, @@ -71,9 +71,9 @@ interface GetStateParams { */ storeInSessionStorage?: boolean; /** - * Browser history used for testing + * History instance to use */ - history?: History; + history: History; } interface GetStateReturn { @@ -126,7 +126,7 @@ export function getState({ }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, - history: history ? history : createBrowserHistory(), + history, }); const globalStateInitial = stateStorage.get(GLOBAL_STATE_URL_KEY) as GlobalState; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 56966d6294c9a..c1de704d1c00a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -49,7 +49,6 @@ import { subscribeWithScope, tabifyAggResponse, getAngularModule, - ensureDefaultIndexPattern, redirectWhenMissing, } from '../../kibana_services'; @@ -57,7 +56,7 @@ const { core, chrome, data, - history, + history: getHistory, indexPatterns, filterManager, share, @@ -116,8 +115,9 @@ app.config($routeProvider => { reloadOnSearch: false, resolve: { savedObjects: function($route, Promise) { + const history = getHistory(); const savedSearchId = $route.current.params.id; - return ensureDefaultIndexPattern(core, data, history).then(() => { + return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ @@ -204,6 +204,8 @@ function discoverController( return isDefaultType($scope.indexPattern) ? $scope.indexPattern.timeFieldName : undefined; }; + const history = getHistory(); + const { appStateContainer, startSync: startStateSync, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx index 9a6bd65813d18..fdae2c0c16c9f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx @@ -31,11 +31,11 @@ import { IndexPatternField } from '../../../../../../../../plugins/data/public'; jest.mock('../../../kibana_services', () => ({ getServices: () => ({ - history: { + history: () => ({ location: { search: '', }, - }, + }), capabilities: { visualize: { show: true, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx index 0df14515adc6d..29451c075bcad 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx @@ -36,11 +36,11 @@ import { SavedObject } from '../../../../../../../../core/types'; jest.mock('../../../kibana_services', () => ({ getServices: () => ({ - history: { + history: () => ({ location: { search: '', }, - }, + }), capabilities: { visualize: { show: true, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts index d146d212055b7..968ceeeab73a5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts @@ -125,7 +125,7 @@ export function getVisualizeUrl( services: DiscoverServices ) { const aggsTermSize = services.uiSettings.get('discover:aggs:terms:size'); - const urlParams = parse(services.history.location.search) as Record; + const urlParams = parse(services.history().location.search) as Record; if ( (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index d05e96ccaaf0b..702331529b879 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -31,7 +31,7 @@ import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; import { EmbeddableStart, EmbeddableSetup } from '../../../../../plugins/embeddable/public'; import { getInnerAngularModule, getInnerAngularModuleEmbeddable } from './get_inner_angular'; -import { setAngularModule, setServices, setUrlTracker } from './kibana_services'; +import { getHistory, setAngularModule, setServices, setUrlTracker } from './kibana_services'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { ChartsPluginStart } from '../../../../../plugins/charts/public'; import { buildServices } from './build_services'; @@ -98,6 +98,10 @@ export class DiscoverPlugin implements Plugin { stop: stopUrlTracker, setActiveUrl: setTrackedUrl, } = createKbnUrlTracker({ + // we pass getter here instead of plain `history`, + // so history is lazily created (when app is mounted) + // this prevents redundant `#` when not in discover app + getHistory, baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/discover', storageKey: `lastUrl:${core.http.basePath.get()}:discover`, @@ -143,6 +147,9 @@ export class DiscoverPlugin implements Plugin { await this.initializeServices(); await this.initializeInnerAngular(); + // make sure the index pattern list is up to date + const [, { data: dataStart }] = await core.getStartServices(); + await dataStart.indexPatterns.clearCache(); const { renderApp } = await import('./np_ready/application'); const unmount = await renderApp(innerAngularName, params.element); return () => { @@ -174,7 +181,7 @@ export class DiscoverPlugin implements Plugin { if (this.servicesInitialized) { return { core, plugins }; } - const services = await buildServices(core, plugins); + const services = await buildServices(core, plugins, getHistory); setServices(services); this.servicesInitialized = true; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 09a06bd8827ce..ed65db10e0acb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -149,6 +149,7 @@ exports[`CreateIndexPatternWizard renders time field step when step is set to 2 indexPatternsService={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 0376df6bbdc58..0bf7c7f0bdfbe 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -6,156 +6,7 @@ role="region" aria-label="{{::'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}" > - -

- -
-

- - - - - - - - - - - - - - - {{tag.name}} - - - - -

- -
- -
-

- - - - - -

-
- -
- - -
-
- - -
- -
-
- -
-
-
- - -
- -
- - -
-
-
- - -
-
- -
- -
- -
- -
-
- - -
-
- -
- -
-
+
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js deleted file mode 100644 index 3239a17f109e4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ /dev/null @@ -1,511 +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. - */ - -import _ from 'lodash'; -import { HashRouter } from 'react-router-dom'; -import { IndexHeader } from './index_header'; -import { CreateEditField } from './create_edit_field'; -import { docTitle } from 'ui/doc_title'; -import { KbnUrlProvider } from 'ui/url'; -import { IndicesEditSectionsProvider } from './edit_sections'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import template from './edit_index_pattern.html'; -import createEditFieldtemplate from './create_edit_field.html'; -import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public'; -import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { SourceFiltersTable } from './source_filters_table'; -import { IndexedFieldsTable } from './indexed_fields_table'; -import { ScriptedFieldsTable } from './scripted_fields_table'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; -import { - getEditBreadcrumbs, - getEditFieldBreadcrumbs, - getCreateFieldBreadcrumbs, -} from '../breadcrumbs'; -import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from './constants'; -import { createEditIndexPatternPageStateContainer } from './edit_index_pattern_state_container'; - -const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable'; -const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable'; -const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable'; -const REACT_INDEX_HEADER_DOM_ELEMENT_ID = 'reactIndexHeader'; - -const EDIT_FIELD_PATH = '/management/kibana/index_patterns/{{indexPattern.id}}/field/{{name}}'; - -function updateSourceFiltersTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroySourceFiltersTable() { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateScriptedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route), - }} - onRemoveField={() => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroyScriptedFieldsTable() { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateIndexedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getFieldInfo: $scope.getFieldInfo, - }} - /> - , - node - ); - }); -} - -function destroyIndexedFieldsTable() { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function destroyIndexHeader() { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function renderIndexHeader($scope, config) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); -} - -function handleTabChange($scope, newTab) { - destroyIndexedFieldsTable(); - destroySourceFiltersTable(); - destroyScriptedFieldsTable(); - updateTables($scope, newTab); -} - -function updateTables($scope, currentTab) { - switch (currentTab) { - case TAB_SCRIPTED_FIELDS: - return updateScriptedFieldsTable($scope); - case TAB_INDEXED_FIELDS: - return updateIndexedFieldsTable($scope); - case TAB_SOURCE_FILTERS: - return updateSourceFiltersTable($scope); - } -} - -uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { - template, - k7Breadcrumbs: getEditBreadcrumbs, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, -}); - -uiModules - .get('apps/management') - .controller('managementIndexPatternsEdit', function( - $scope, - $location, - $route, - Promise, - config, - Private - ) { - const { - startSyncingState, - stopSyncingState, - setCurrentTab, - getCurrentTab, - state$, - } = createEditIndexPatternPageStateContainer({ - useHashedUrl: config.get('state:storeInSessionStorage'), - defaultTab: TAB_INDEXED_FIELDS, - }); - - $scope.getCurrentTab = getCurrentTab; - $scope.setCurrentTab = setCurrentTab; - - const stateChangedSub = subscribeWithScope( - $scope, - state$, - { - next: ({ tab }) => { - handleTabChange($scope, tab); - }, - }, - fatalError - ); - - handleTabChange($scope, getCurrentTab()); // setup initial tab depending on initial tab state - - startSyncingState(); // starts syncing state between state container and url - - const destroyState = () => { - stateChangedSub.unsubscribe(); - stopSyncingState(); - }; - - $scope.fieldWildcardMatcher = (...args) => - fieldWildcardMatcher(...args, config.get('metaFields')); - $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); - $scope.kbnUrl = Private(KbnUrlProvider); - $scope.indexPattern = $route.current.locals.indexPattern; - $scope.indexPatternListProvider = npStart.plugins.indexPatternManagement.list; - $scope.indexPattern.tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( - $scope.indexPattern, - $scope.indexPattern.id === config.get('defaultIndex') - ); - $scope.getFieldInfo = npStart.plugins.indexPatternManagement.list.getFieldInfo; - docTitle.change($scope.indexPattern.title); - - const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => { - return pattern.id !== $scope.indexPattern.id; - }); - - $scope.$watch('indexPattern.fields', function() { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - $scope.refreshFilters(); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - }); - - $scope.refreshFilters = function() { - const indexedFieldTypes = []; - const scriptedFieldLanguages = []; - $scope.indexPattern.fields.forEach(field => { - if (field.scripted) { - scriptedFieldLanguages.push(field.lang); - } else { - indexedFieldTypes.push(field.type); - } - }); - - $scope.indexedFieldTypes = _.unique(indexedFieldTypes); - $scope.scriptedFieldLanguages = _.unique(scriptedFieldLanguages); - }; - - $scope.changeFilter = function(filter, val) { - $scope[filter] = val || ''; // null causes filter to check for null explicitly - }; - - $scope.$watchCollection('indexPattern.fields', function() { - $scope.conflictFields = $scope.indexPattern.fields.filter(field => field.type === 'conflict'); - }); - - $scope.refreshFields = function() { - const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { - defaultMessage: 'This action resets the popularity counter of each field.', - }); - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { - defaultMessage: 'Refresh', - }), - title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { - defaultMessage: 'Refresh field list?', - }), - }; - - npStart.core.overlays - .openConfirm(confirmMessage, confirmModalOptions) - .then(async isConfirmed => { - if (isConfirmed) { - await $scope.indexPattern.init(true); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - } - }); - }; - - $scope.removePattern = function() { - function doRemove() { - if ($scope.indexPattern.id === config.get('defaultIndex')) { - config.remove('defaultIndex'); - - if (otherPatterns.length) { - config.set('defaultIndex', otherPatterns[0].id); - } - } - - Promise.resolve($scope.indexPattern.destroy()) - .then(function() { - $location.url('/management/kibana/index_patterns'); - }) - .catch(fatalError); - } - - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { - defaultMessage: 'Delete', - }), - title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { - defaultMessage: 'Delete index pattern?', - }), - }; - - npStart.core.overlays.openConfirm('', confirmModalOptions).then(isConfirmed => { - if (isConfirmed) { - doRemove(); - } - }); - }; - - $scope.setDefaultPattern = function() { - config.set('defaultIndex', $scope.indexPattern.id); - }; - - $scope.setIndexPatternsTimeField = function(field) { - if (field.type !== 'date') { - const errorMessage = i18n.translate('kbn.management.editIndexPattern.notDateErrorMessage', { - defaultMessage: 'That field is a {fieldType} not a date.', - values: { fieldType: field.type }, - }); - toastNotifications.addDanger(errorMessage); - return; - } - $scope.indexPattern.timeFieldName = field.name; - return $scope.indexPattern.save(); - }; - - $scope.$watch('fieldFilter', () => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - - if ($scope.fieldFilter === undefined) { - return; - } - - updateTables($scope, getCurrentTab()); - }); - - $scope.$watch('indexedFieldTypeFilter', () => { - if ($scope.indexedFieldTypeFilter !== undefined && getCurrentTab() === TAB_INDEXED_FIELDS) { - updateIndexedFieldsTable($scope); - } - }); - - $scope.$watch('scriptedFieldLanguageFilter', () => { - if ( - $scope.scriptedFieldLanguageFilter !== undefined && - getCurrentTab() === TAB_SCRIPTED_FIELDS - ) { - updateScriptedFieldsTable($scope); - } - }); - - $scope.$on('$destroy', () => { - destroyIndexedFieldsTable(); - destroyScriptedFieldsTable(); - destroySourceFiltersTable(); - destroyIndexHeader(); - destroyState(); - }); - - renderIndexHeader($scope, config); - }); - -// routes for create edit field. Will be removed after migartion all component to react. -const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; -const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - if (!node) { - return; - } - - render( - - - - - , - node - ); - }); -}; - -const destroyCreateEditField = () => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - node && unmountComponentAtNode(node); -}; - -uiRoutes - .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { - mode: 'edit', - k7Breadcrumbs: getEditFieldBreadcrumbs, - }) - .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { - mode: 'create', - k7Breadcrumbs: getCreateFieldBreadcrumbs, - }) - .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { - template: createEditFieldtemplate, - mapBreadcrumbs($route, breadcrumbs) { - const { indexPattern } = $route.current.locals; - return breadcrumbs.map(crumb => { - if (crumb.id !== indexPattern.id) { - return crumb; - } - - return { - ...crumb, - display: indexPattern.title, - }; - }); - }, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, - controllerAs: 'fieldSettings', - controller: function FieldEditorPageController($scope, $route, $http, Private, config) { - const getConfig = (...args) => config.get(...args); - const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); - - renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); - - $scope.$on('$destroy', () => { - destroyCreateEditField(); - }); - }, - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx new file mode 100644 index 0000000000000..e869ac84c2db2 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx @@ -0,0 +1,238 @@ +/* + * 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. + */ + +import { filter } from 'lodash'; +import React, { useEffect, useState, useCallback } from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiText, + EuiLink, + EuiIcon, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../plugins/data/public'; +import { + ChromeDocTitle, + NotificationsStart, + OverlayStart, +} from '../../../../../../../../core/public'; +import { IndexPatternManagementStart } from '../../../../../../../../plugins/index_pattern_management/public'; +import { Tabs } from './tabs'; +import { IndexHeader } from './index_header'; + +interface EditIndexPatternProps extends RouteComponentProps { + indexPattern: IndexPattern; + indexPatterns: IndexPattern[]; + config: Record; + services: { + notifications: NotificationsStart; + docTitle: ChromeDocTitle; + overlays: OverlayStart; + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const mappingAPILink = i18n.translate( + 'kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink', + { + defaultMessage: 'Mapping API', + } +); + +const mappingConflictHeader = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictHeader', + { + defaultMessage: 'Mapping conflict', + } +); + +const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { + defaultMessage: 'This action resets the popularity counter of each field.', +}); + +const confirmModalOptionsRefresh = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { + defaultMessage: 'Refresh', + }), + title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { + defaultMessage: 'Refresh field list?', + }), +}; + +const confirmModalOptionsDelete = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { + defaultMessage: 'Delete', + }), + title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { + defaultMessage: 'Delete index pattern?', + }), +}; + +export const EditIndexPattern = withRouter( + ({ indexPattern, indexPatterns, config, services, history, location }: EditIndexPatternProps) => { + const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); + const [conflictedFields, setConflictedFields] = useState( + indexPattern.fields.filter(field => field.type === 'conflict') + ); + const [defaultIndex, setDefaultIndex] = useState(config.get('defaultIndex')); + const [tags, setTags] = useState([]); + + useEffect(() => { + setFields(indexPattern.getNonScriptedFields()); + setConflictedFields(indexPattern.fields.filter(field => field.type === 'conflict')); + }, [indexPattern, indexPattern.fields]); + + useEffect(() => { + const indexPatternTags = + services.indexPatternManagement.list.getIndexPatternTags( + indexPattern, + indexPattern.id === defaultIndex + ) || []; + setTags(indexPatternTags); + }, [defaultIndex, indexPattern, services.indexPatternManagement.list]); + + const setDefaultPattern = useCallback(() => { + config.set('defaultIndex', indexPattern.id); + setDefaultIndex(indexPattern.id || ''); + }, [config, indexPattern.id]); + + const refreshFields = () => { + services.overlays + .openConfirm(confirmMessage, confirmModalOptionsRefresh) + .then(async isConfirmed => { + if (isConfirmed) { + await indexPattern.init(true); + setFields(indexPattern.getNonScriptedFields()); + } + }); + }; + + const removePattern = () => { + function doRemove() { + if (indexPattern.id === defaultIndex) { + config.remove('defaultIndex'); + const otherPatterns = filter(indexPatterns, pattern => { + return pattern.id !== indexPattern.id; + }); + + if (otherPatterns.length) { + config.set('defaultIndex', otherPatterns[0].id); + } + } + + Promise.resolve(indexPattern.destroy()).then(function() { + history.push('/management/kibana/index_patterns'); + }); + } + + services.overlays.openConfirm('', confirmModalOptionsDelete).then(isConfirmed => { + if (isConfirmed) { + doRemove(); + } + }); + }; + + const timeFilterHeader = i18n.translate('kbn.management.editIndexPattern.timeFilterHeader', { + defaultMessage: "Time Filter field name: '{timeFieldName}'", + values: { timeFieldName: indexPattern.timeFieldName }, + }); + + const mappingConflictLabel = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictLabel', + { + defaultMessage: + '{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.', + values: { conflictFieldsLength: conflictedFields.length }, + } + ); + + services.docTitle.change(indexPattern.title); + + const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0)); + + return ( + + + + + {showTagsSection && ( + + {Boolean(indexPattern.timeFieldName) && ( + + {timeFilterHeader} + + )} + {tags.map((tag: any) => ( + + {tag.name} + + ))} + + )} + + +

+ {indexPattern.title} }} + />{' '} + + {mappingAPILink} + + +

+
+ {conflictedFields.length > 0 && ( + +

{mappingConflictLabel}

+
+ )} +
+ + + +
+ ); + } +); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts index 473417a7aabd6..5723a596f95d5 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts @@ -25,7 +25,7 @@ import { } from '../../../../../../../../plugins/kibana_utils/public'; interface IEditIndexPatternState { - tab: string; // TODO: type those 3 tabs with enum, when edit_index_pattern.js migrated to ts + tab: string; } /** @@ -38,7 +38,6 @@ export function createEditIndexPatternPageStateContainer({ defaultTab: string; useHashedUrl: boolean; }) { - // until angular is used as shell - use hash history const history = createHashHistory(); // query param to store app state at const stateStorageKey = '_a'; @@ -78,12 +77,10 @@ export function createEditIndexPatternPageStateContainer({ // makes sure initial url is the same as initial state (this is not really required) kbnUrlStateStorage.set(stateStorageKey, stateContainer.getState(), { replace: true }); - // expose api needed for Controller return { startSyncingState: start, stopSyncingState: stop, setCurrentTab: (newTab: string) => stateContainer.transitions.setTab(newTab), getCurrentTab: () => stateContainer.selectors.tab(), - state$: stateContainer.state$, }; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js deleted file mode 100644 index f0220b2f798e5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js +++ /dev/null @@ -1,80 +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. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -function filterBy(items, key, filter) { - const lowercaseFilter = (filter || '').toLowerCase(); - return items.filter(item => item[key].toLowerCase().includes(lowercaseFilter)); -} - -function getCounts(fields, sourceFilters, fieldFilter = '') { - const fieldCount = _.countBy(filterBy(fields, 'name', fieldFilter), function(field) { - return field.scripted ? 'scripted' : 'indexed'; - }); - - _.defaults(fieldCount, { - indexed: 0, - scripted: 0, - sourceFilters: sourceFilters ? filterBy(sourceFilters, 'value', fieldFilter).length : 0, - }); - - return fieldCount; -} - -export function IndicesEditSectionsProvider() { - return function(indexPattern, fieldFilter, indexPatternListProvider) { - const totalCount = getCounts(indexPattern.fields, indexPattern.sourceFilters); - const filteredCount = getCounts(indexPattern.fields, indexPattern.sourceFilters, fieldFilter); - - const editSections = []; - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { - defaultMessage: 'Fields', - }), - index: 'indexedFields', - count: filteredCount.indexed, - totalCount: totalCount.indexed, - }); - - if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { - defaultMessage: 'Scripted fields', - }), - index: 'scriptedFields', - count: filteredCount.scripted, - totalCount: totalCount.scripted, - }); - } - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { - defaultMessage: 'Source filters', - }), - index: 'sourceFilters', - count: filteredCount.sourceFilters, - totalCount: totalCount.sourceFilters, - }); - - return editSections; - }; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html deleted file mode 100644 index c14dcd3f3a8d5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - - - -
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js index 6beaee60b3788..e05aea3678fe2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js @@ -17,4 +17,159 @@ * under the License. */ -import './edit_index_pattern'; +import React from 'react'; +import { HashRouter } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; +import uiRoutes from 'ui/routes'; +import { uiModules } from 'ui/modules'; +import { I18nContext } from 'ui/i18n'; +import { npStart } from 'ui/new_platform'; +import template from './edit_index_pattern.html'; +import createEditFieldtemplate from './create_edit_field.html'; +import { + getEditBreadcrumbs, + getEditFieldBreadcrumbs, + getCreateFieldBreadcrumbs, +} from '../breadcrumbs'; +import { EditIndexPattern } from './edit_index_pattern'; +import { CreateEditField } from './create_edit_field'; + +const REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID = 'reactEditIndexPattern'; + +function destroyEditIndexPattern() { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + +function renderEditIndexPattern($scope, config, $route) { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +} + +uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { + template, + k7Breadcrumbs: getEditBreadcrumbs, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, +}); + +uiModules + .get('apps/management') + .controller('managementIndexPatternsEdit', function($scope, $route, config) { + $scope.$on('$destroy', () => { + destroyEditIndexPattern(); + }); + + renderEditIndexPattern($scope, config, $route); + }); + +// routes for create edit field. Will be removed after migartion all component to react. +const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; +const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +}; + +const destroyCreateEditField = () => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + node && unmountComponentAtNode(node); +}; + +uiRoutes + .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { + mode: 'edit', + k7Breadcrumbs: getEditFieldBreadcrumbs, + }) + .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { + mode: 'create', + k7Breadcrumbs: getCreateFieldBreadcrumbs, + }) + .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { + template: createEditFieldtemplate, + mapBreadcrumbs($route, breadcrumbs) { + const { indexPattern } = $route.current.locals; + return breadcrumbs.map(crumb => { + if (crumb.id !== indexPattern.id) { + return crumb; + } + + return { + ...crumb, + display: indexPattern.title, + }; + }); + }, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, + controllerAs: 'fieldSettings', + controller: function FieldEditorPageController($scope, $route, $http, Private, config) { + const getConfig = (...args) => config.get(...args); + const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); + + renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); + + $scope.$on('$destroy', () => { + destroyCreateEditField(); + }); + }, + }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx index deac85d9a32e9..a06671ef6a470 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx @@ -92,7 +92,7 @@ export function IndexHeader({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 7c2bb565615d7..c69063967b1e2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -19,7 +19,11 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; -import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { + IndexPatternField, + IIndexPattern, + IFieldType, +} from '../../../../../../../../../plugins/data/public'; import { Table } from './components/table'; import { getFieldFormat } from './lib'; import { IndexedFieldItem } from './types'; @@ -31,7 +35,7 @@ interface IndexedFieldsTableProps { indexedFieldTypeFilter?: string; helpers: { redirectToRoute: (obj: any) => void; - getFieldInfo: (indexPattern: IIndexPattern, field: string) => string[]; + getFieldInfo: (indexPattern: IIndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; } @@ -76,7 +80,7 @@ export class IndexedFieldsTable extends Component< indexPattern: field.indexPattern, format: getFieldFormat(indexPattern, field.name), excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, - info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name), + info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), }; })) || [] diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap index 569b75c848c52..202b09ddc6066 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
void; } @@ -136,14 +136,19 @@ export class ScriptedFieldsTable extends Component< }; render() { - const { helpers, indexPattern } = this.props; + const { indexPattern } = this.props; const { fieldToDelete, deprecatedLangsInUse } = this.state; const items = this.getFilteredItems(); return ( <> -
+
{ + indexPattern: IndexPattern; + config: Record; + fields: IndexPatternField[]; + services: { + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const filterAriaLabel = i18n.translate('kbn.management.editIndexPattern.fields.filterAria', { + defaultMessage: 'Filter', +}); + +const filterPlaceholder = i18n.translate( + 'kbn.management.editIndexPattern.fields.filterPlaceholder', + { + defaultMessage: 'Filter', + } +); + +export function Tabs({ config, indexPattern, fields, services, history, location }: TabsProps) { + const [fieldFilter, setFieldFilter] = useState(''); + const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); + const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState(''); + const [indexedFieldTypes, setIndexedFieldType] = useState([]); + const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState([]); + const [syncingStateFunc, setSyncingStateFunc] = useState({ + getCurrentTab: () => TAB_INDEXED_FIELDS, + }); + + const refreshFilters = useCallback(() => { + const tempIndexedFieldTypes: string[] = []; + const tempScriptedFieldLanguages: string[] = []; + indexPattern.fields.forEach(field => { + if (field.scripted) { + if (field.lang) { + tempScriptedFieldLanguages.push(field.lang); + } + } else { + tempIndexedFieldTypes.push(field.type); + } + }); + + setIndexedFieldType(convertToEuiSelectOption(tempIndexedFieldTypes, 'indexedFiledTypes')); + setScriptedFieldLanguages( + convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages') + ); + }, [indexPattern]); + + useEffect(() => { + refreshFilters(); + }, [indexPattern, indexPattern.fields, refreshFilters]); + + const fieldWildcardMatcherDecorated = useCallback( + (filters: string[]) => fieldWildcardMatcher(filters, config.get('metaFields')), + [config] + ); + + const getFilterSection = useCallback( + (type: string) => { + return ( + + + setFieldFilter(e.target.value)} + data-test-subj="indexPatternFieldFilter" + aria-label={filterAriaLabel} + /> + + {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( + + setIndexedFieldTypeFilter(e.target.value)} + data-test-subj="indexedFieldTypeFilterDropdown" + /> + + )} + {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( + + setScriptedFieldLanguageFilter(e.target.value)} + data-test-subj="scriptedFieldLanguageFilterDropdown" + /> + + )} + + ); + }, + [ + fieldFilter, + indexedFieldTypeFilter, + indexedFieldTypes, + scriptedFieldLanguageFilter, + scriptedFieldLanguages, + ] + ); + + const getContent = useCallback( + (type: string) => { + switch (type) { + case TAB_INDEXED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + getFieldInfo: services.indexPatternManagement.list.getFieldInfo, + }} + /> + + ); + case TAB_SCRIPTED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + }} + onRemoveField={refreshFilters} + /> + + ); + case TAB_SOURCE_FILTERS: + return ( + + + {getFilterSection(type)} + + + + ); + } + }, + [ + fieldFilter, + fieldWildcardMatcherDecorated, + fields, + getFilterSection, + history, + indexPattern, + indexedFieldTypeFilter, + refreshFilters, + scriptedFieldLanguageFilter, + services.indexPatternManagement.list.getFieldInfo, + ] + ); + + const euiTabs: EuiTabbedContentTab[] = useMemo( + () => + getTabs(indexPattern, fieldFilter, services.indexPatternManagement.list).map( + (tab: Pick) => { + return { + ...tab, + content: getContent(tab.id), + }; + } + ), + [fieldFilter, getContent, indexPattern, services.indexPatternManagement.list] + ); + + const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id); + + useEffect(() => { + const { + startSyncingState, + stopSyncingState, + setCurrentTab, + getCurrentTab, + } = createEditIndexPatternPageStateContainer({ + useHashedUrl: config.get('state:storeInSessionStorage'), + defaultTab: TAB_INDEXED_FIELDS, + }); + + startSyncingState(); + setSyncingStateFunc({ + setCurrentTab, + getCurrentTab, + }); + setSelectedTabId(getCurrentTab()); + + return () => { + stopSyncingState(); + }; + }, [config]); + + return ( + tab.id === selectedTabId)} + onTabClick={tab => { + setSelectedTabId(tab.id); + syncingStateFunc.setCurrentTab(tab.id); + }} + /> + ); +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts new file mode 100644 index 0000000000000..bdb1436c37efb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts @@ -0,0 +1,146 @@ +/* + * 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. + */ + +import { Dictionary, countBy, defaults, unique } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public'; +import { IndexPatternManagementStart } from '../../../../../../../../../plugins/index_pattern_management/public'; +import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants'; + +function filterByName(items: IndexPatternField[], filter: string) { + const lowercaseFilter = (filter || '').toLowerCase(); + return items.filter(item => item.name.toLowerCase().includes(lowercaseFilter)); +} + +function getCounts( + fields: IndexPatternField[], + sourceFilters: { + excludes: string[]; + }, + fieldFilter = '' +) { + const fieldCount = countBy(filterByName(fields, fieldFilter), function(field) { + return field.scripted ? 'scripted' : 'indexed'; + }); + + defaults(fieldCount, { + indexed: 0, + scripted: 0, + sourceFilters: sourceFilters.excludes + ? sourceFilters.excludes.filter(value => + value.toLowerCase().includes(fieldFilter.toLowerCase()) + ).length + : 0, + }); + + return fieldCount; +} + +function getTitle(type: string, filteredCount: Dictionary, totalCount: Dictionary) { + let title = ''; + switch (type) { + case 'indexed': + title = i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { + defaultMessage: 'Fields', + }); + break; + case 'scripted': + title = i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { + defaultMessage: 'Scripted fields', + }); + break; + case 'sourceFilters': + title = i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { + defaultMessage: 'Source filters', + }); + break; + } + const count = ` (${ + filteredCount[type] === totalCount[type] + ? filteredCount[type] + : filteredCount[type] + ' / ' + totalCount[type] + })`; + return title + count; +} + +export function getTabs( + indexPattern: IndexPattern, + fieldFilter: string, + indexPatternListProvider: IndexPatternManagementStart['list'] +) { + const totalCount = getCounts(indexPattern.fields, indexPattern.getSourceFiltering()); + const filteredCount = getCounts( + indexPattern.fields, + indexPattern.getSourceFiltering(), + fieldFilter + ); + + const tabs = []; + + tabs.push({ + name: getTitle('indexed', filteredCount, totalCount), + id: TAB_INDEXED_FIELDS, + }); + + if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { + tabs.push({ + name: getTitle('scripted', filteredCount, totalCount), + id: TAB_SCRIPTED_FIELDS, + }); + } + + tabs.push({ + name: getTitle('sourceFilters', filteredCount, totalCount), + id: TAB_SOURCE_FILTERS, + }); + + return tabs; +} + +export function getPath(field: IndexPatternField) { + return `/management/kibana/index_patterns/${field.indexPattern?.id}/field/${field.name}`; +} + +const allTypesDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allTypesDropDown', { + defaultMessage: 'All field types', +}); + +const allLangsDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allLangsDropDown', { + defaultMessage: 'All languages', +}); + +export function convertToEuiSelectOption(options: string[], type: string) { + const euiOptions = + options.length > 0 + ? [ + { + value: '', + text: type === 'scriptedFieldLanguages' ? allLangsDropDown : allTypesDropDown, + }, + ] + : []; + return euiOptions.concat( + unique(options).map(option => { + return { + value: option, + text: option, + }; + }) + ); +} diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js index 5e78047088d2a..e1966a9e8b266 100644 --- a/src/legacy/core_plugins/tests_bundle/index.js +++ b/src/legacy/core_plugins/tests_bundle/index.js @@ -148,6 +148,19 @@ export default kibana => { .type('text/css'); }, }); + + // Sets global variables normally set by the bootstrap.js script + kbnServer.server.route({ + path: '/test_bundle/karma/globals.js', + method: 'GET', + async handler(req, h) { + const basePath = config.get('server.basePath'); + + const file = `window.__kbnPublicPath__ = { 'kbn-ui-shared-deps': "${basePath}/bundles/kbn-ui-shared-deps/" };`; + + return h.response(file).header('content-type', 'application/json'); + }, + }); }, __globalImportAliases__: { diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 3e3dc284671da..f075d8365c299 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -135,7 +135,16 @@ const coreSystem = new CoreSystem({ }, }, rootDomElement, - useLegacyTestHarness: true, + requireLegacyBootstrapModule: () => { + // wrapped in NODE_ENV check so the 'ui/test_harness' module + // is not included in the distributable + if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true') { + return require('ui/test_harness'); + } + + throw new Error('tests bundle is not available in the distributable'); + }, + requireNewPlatformShimModule: () => require('ui/new_platform'), requireLegacyFiles: () => { ${bundle.getRequires().join('\n ')} } diff --git a/src/legacy/core_plugins/vis_type_table/index.ts b/src/legacy/core_plugins/vis_type_table/index.ts deleted file mode 100644 index 04ca9da7de32b..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/index.ts +++ /dev/null @@ -1,44 +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. - */ - -import { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const tableVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'table_vis', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default tableVisPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_table/package.json b/src/legacy/core_plugins/vis_type_table/package.json deleted file mode 100644 index 2809b0e047836..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "table_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js deleted file mode 100644 index a23407a599ae2..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ /dev/null @@ -1,482 +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. - */ - -import $ from 'jquery'; -import moment from 'moment'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { npStart } from '../../legacy_imports'; -import { round } from 'lodash'; -import { getAngularModule } from '../../get_inner_angular'; -import { initTableVisLegacyModule } from '../../table_vis_legacy_module'; -import { tabifiedData } from './tabified_data'; - -describe('Table Vis - AggTable Directive', function() { - let $rootScope; - let $compile; - let settings; - - const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', npStart.core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - - beforeEach(ngMock.module('kibana/table_vis')); - beforeEach( - ngMock.inject(function($injector, config) { - settings = config; - - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }) - ); - - let $scope; - beforeEach(function() { - $scope = $rootScope.$new(); - }); - afterEach(function() { - $scope.$destroy(); - }); - - it('renders a simple response properly', function() { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.table = tabifiedData.metricOnly.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).to.be(1); - expect($el.find('td').length).to.be(1); - expect($el.find('td').text()).to.eql('1,000'); - }); - - it('renders nothing if the table is empty', function() { - $scope.dimensions = {}; - $scope.table = null; - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).to.be(0); - }); - - it('renders a complex response properly', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - const $el = $(''); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tbody').length).to.be(1); - - const $rows = $el.find('tbody tr'); - expect($rows.length).to.be.greaterThan(0); - - function validBytes(str) { - const num = str.replace(/,/g, ''); - if (num !== '-') { - expect(num).to.match(/^\d+$/); - } - } - - $rows.each(function() { - // 6 cells in every row - const $cells = $(this).find('td'); - expect($cells.length).to.be(6); - - const txts = $cells.map(function() { - return $(this) - .text() - .trim(); - }); - - // two character country code - expect(txts[0]).to.match(/^(png|jpg|gif|html|css)$/); - validBytes(txts[1]); - - // country - expect(txts[2]).to.match(/^\w\w$/); - validBytes(txts[3]); - - // os - expect(txts[4]).to.match(/^(win|mac|linux)$/); - validBytes(txts[5]); - }); - }); - - describe('renders totals row', function() { - async function totalsRowTest(totalFunc, expected) { - function setDefaultTimezone() { - moment.tz.setDefault(settings.get('dateFormat:tz')); - } - - const off = $scope.$on('change:config.dateFormat:tz', setDefaultTimezone); - const oldTimezoneSetting = settings.get('dateFormat:tz'); - settings.set('dateFormat:tz', 'UTC'); - - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.showTotal = true; - $scope.totalFunc = totalFunc; - const $el = $(``); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tfoot').length).to.be(1); - - const $rows = $el.find('tfoot tr'); - expect($rows.length).to.be(1); - - const $cells = $($rows[0]).find('th'); - expect($cells.length).to.be(6); - - for (let i = 0; i < 6; i++) { - expect( - $($cells[i]) - .text() - .trim() - ).to.be(expected[i]); - } - settings.set('dateFormat:tz', oldTimezoneSetting); - off(); - } - it('as count', async function() { - await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); - }); - it('as min', async function() { - await totalsRowTest('min', [ - '', - '2014-09-28', - '9,283', - 'Sep 28, 2014 @ 00:00:00.000', - '1', - '11', - ]); - }); - it('as max', async function() { - await totalsRowTest('max', [ - '', - '2014-10-03', - '220,943', - 'Oct 3, 2014 @ 00:00:00.000', - '239', - '837', - ]); - }); - it('as avg', async function() { - await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); - }); - it('as sum', async function() { - await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); - }); - }); - - describe('aggTable.toCsv()', function() { - it('escapes rows and columns properly', function() { - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - expect(aggTable.toCsv()).to.be( - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' - ); - }); - - it('exports rows and columns properly', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - const raw = aggTable.toCsv(false); - expect(raw).to.be( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - 'png,IT,win,412032,9299,0' + - '\r\n' + - 'png,IT,mac,412032,9299,9299' + - '\r\n' + - 'png,US,linux,412032,8293,3992' + - '\r\n' + - 'png,US,mac,412032,8293,3029' + - '\r\n' + - 'css,MX,win,412032,9299,4992' + - '\r\n' + - 'css,MX,mac,412032,9299,5892' + - '\r\n' + - 'css,US,linux,412032,8293,3992' + - '\r\n' + - 'css,US,mac,412032,8293,3029' + - '\r\n' + - 'html,CN,win,412032,9299,4992' + - '\r\n' + - 'html,CN,mac,412032,9299,5892' + - '\r\n' + - 'html,FR,win,412032,8293,3992' + - '\r\n' + - 'html,FR,mac,412032,8293,3029' + - '\r\n' - ); - }); - - it('exports formatted rows and columns properly', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - // Create our own converter since the ones we use for tests don't actually transform the provided value - $tableScope.formattedColumns[0].formatter.convert = v => `${v}_formatted`; - - const formatted = aggTable.toCsv(true); - expect(formatted).to.be( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - '"png_formatted",IT,win,412032,9299,0' + - '\r\n' + - '"png_formatted",IT,mac,412032,9299,9299' + - '\r\n' + - '"png_formatted",US,linux,412032,8293,3992' + - '\r\n' + - '"png_formatted",US,mac,412032,8293,3029' + - '\r\n' + - '"css_formatted",MX,win,412032,9299,4992' + - '\r\n' + - '"css_formatted",MX,mac,412032,9299,5892' + - '\r\n' + - '"css_formatted",US,linux,412032,8293,3992' + - '\r\n' + - '"css_formatted",US,mac,412032,8293,3029' + - '\r\n' + - '"html_formatted",CN,win,412032,9299,4992' + - '\r\n' + - '"html_formatted",CN,mac,412032,9299,5892' + - '\r\n' + - '"html_formatted",FR,win,412032,8293,3992' + - '\r\n' + - '"html_formatted",FR,mac,412032,8293,3029' + - '\r\n' - ); - }); - }); - - it('renders percentage columns', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.percentageCol = 'Average bytes'; - - const $el = $(``); - - $compile($el)($scope); - $scope.$digest(); - - const $headings = $el.find('th'); - expect($headings.length).to.be(7); - expect( - $headings - .eq(3) - .text() - .trim() - ).to.be('Average bytes percentages'); - - const countColId = $scope.table.columns.find(col => col.name === $scope.percentageCol).id; - const counts = $scope.table.rows.map(row => row[countColId]); - const total = counts.reduce((sum, curr) => sum + curr, 0); - const $percentageColValues = $el.find('tbody tr').map((i, el) => - $(el) - .find('td') - .eq(3) - .text() - ); - - $percentageColValues.each((i, value) => { - const percentage = `${round((counts[i] / total) * 100, 1)}%`; - expect(value).to.be(percentage); - }); - }); - - describe('aggTable.exportAsCsv()', function() { - let origBlob; - function FakeBlob(slices, opts) { - this.slices = slices; - this.opts = opts; - } - - beforeEach(function() { - origBlob = window.Blob; - window.Blob = FakeBlob; - }); - - afterEach(function() { - window.Blob = origBlob; - }); - - it('calls _saveAs properly', function() { - const $el = $compile('')($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - - const saveAs = sinon.stub(aggTable, '_saveAs'); - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - aggTable.csv.filename = 'somefilename.csv'; - aggTable.exportAsCsv(); - - expect(saveAs.callCount).to.be(1); - const call = saveAs.getCall(0); - expect(call.args[0]).to.be.a(FakeBlob); - expect(call.args[0].slices).to.eql([ - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', - ]); - expect(call.args[0].opts).to.eql({ - type: 'text/plain;charset=utf-8', - }); - expect(call.args[1]).to.be('somefilename.csv'); - }); - - it('should use the export-title attribute', function() { - const expected = 'export file name'; - const $el = $compile( - `` - )($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [], - rows: [], - }; - $tableScope.exportTitle = expected; - $scope.$digest(); - - expect(aggTable.csv.filename).to.equal(`${expected}.csv`); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/_index.scss b/src/legacy/core_plugins/vis_type_table/public/agg_table/_index.scss deleted file mode 100644 index b19d4a887a7f3..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'agg_table'; diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js deleted file mode 100644 index b9e79f96e4fc1..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js +++ /dev/null @@ -1,272 +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. - */ -import _ from 'lodash'; -import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../services'; -import { i18n } from '@kbn/i18n'; - -export function KbnAggTable(config, RecursionHelper) { - return { - restrict: 'E', - template: aggTableTemplate, - scope: { - table: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - controllerAs: 'aggTable', - compile: function($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el); - }, - controller: function($scope) { - const self = this; - - self._saveAs = require('@elastic/filesaver').saveAs; - self.csv = { - separator: config.get('csv:separator'), - quoteValues: config.get('csv:quoteValues'), - }; - - self.exportAsCsv = function(formatted) { - const csv = new Blob([self.toCsv(formatted)], { type: 'text/plain;charset=utf-8' }); - self._saveAs(csv, self.csv.filename); - }; - - self.toCsv = function(formatted) { - const rows = $scope.table.rows; - const columns = formatted ? $scope.formattedColumns : $scope.table.columns; - const nonAlphaNumRE = /[^a-zA-Z0-9]/; - const allDoubleQuoteRE = /"/g; - - function escape(val) { - if (!formatted && _.isObject(val)) val = val.valueOf(); - val = String(val); - if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { - val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; - } - return val; - } - - // escape each cell in each row - const csvRows = rows.map(function(row) { - return Object.entries(row).map(([k, v]) => { - const column = columns.find(c => c.id === k); - if (formatted && column) { - return escape(column.formatter.convert(v)); - } - return escape(v); - }); - }); - - // add the columns to the rows - csvRows.unshift( - columns.map(function(col) { - return escape(formatted ? col.title : col.name); - }) - ); - - return csvRows - .map(function(row) { - return row.join(self.csv.separator) + '\r\n'; - }) - .join(''); - }; - - $scope.$watchMulti( - ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], - function() { - const { table, exportTitle, percentageCol } = $scope; - const showPercentage = percentageCol !== ''; - - if (!table) { - $scope.rows = null; - $scope.formattedColumns = null; - return; - } - - self.csv.filename = (exportTitle || table.title || 'table') + '.csv'; - $scope.rows = table.rows; - $scope.formattedColumns = []; - - if (typeof $scope.dimensions === 'undefined') return; - - const { buckets, metrics, splitColumn } = $scope.dimensions; - - $scope.formattedColumns = table.columns - .map(function(col, i) { - const isBucket = buckets.find(bucket => bucket.accessor === i); - const isSplitColumn = splitColumn - ? splitColumn.find(splitColumn => splitColumn.accessor === i) - : undefined; - const dimension = - isBucket || isSplitColumn || metrics.find(metric => metric.accessor === i); - - if (!dimension) return; - - const formatter = getFormatService().deserialize(dimension.format); - - const formattedColumn = { - id: col.id, - title: col.name, - formatter: formatter, - filterable: !!isBucket, - }; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } - - const isDate = - dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } - - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = tableRows => { - return _.reduce( - tableRows, - function(prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { - case 'sum': { - if (!isDate) { - const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case 'avg': { - if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - } - break; - } - case 'min': { - const total = _.chain(table.rows) - .map(col.id) - .min() - .value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'max': { - const total = _.chain(table.rows) - .map(col.id) - .max() - .value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - - return formattedColumn; - }) - .filter(column => column); - - if (showPercentage) { - const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); - - // column to show percentage for was removed - if (insertAtIndex < 0) return; - - const { cols, rows } = addPercentageCol( - $scope.formattedColumns, - percentageCol, - table.rows, - insertAtIndex - ); - $scope.rows = rows; - $scope.formattedColumns = cols; - } - } - ); - }, - }; -} - -/** - * @param {[]Object} columns - the formatted columns that will be displayed - * @param {String} title - the title of the column to add to - * @param {[]Object} rows - the row data for the columns - * @param {Number} insertAtIndex - the index to insert the percentage column at - * @returns {Object} - cols and rows for the table to render now included percentage column(s) - */ -function addPercentageCol(columns, title, rows, insertAtIndex) { - const { id, sumTotal } = columns[insertAtIndex]; - const newId = `${id}-percents`; - const formatter = getFormatService().deserialize({ id: 'percent' }); - const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { - defaultMessage: '{title} percentages', - values: { title }, - }); - const newCols = insert(columns, insertAtIndex, { - title: i18nTitle, - id: newId, - formatter, - }); - const newRows = rows.map(row => ({ - [newId]: formatter.convert(row[id] / sumTotal / 100), - ...row, - })); - - return { cols: newCols, rows: newRows }; -} - -function insert(arr, index, ...items) { - const newArray = [...arr]; - newArray.splice(index + 1, 0, ...items); - return newArray; -} diff --git a/src/legacy/core_plugins/vis_type_table/public/index.scss b/src/legacy/core_plugins/vis_type_table/public/index.scss deleted file mode 100644 index 54124ebc42620..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "tbv" to avoid conflicts. -// Examples -// tbvChart -// tbvChart__legend -// tbvChart__legend--small -// tbvChart__legend-isLoading - -@import 'agg_table/index'; -@import 'paginated_table/index'; -@import './table_vis'; diff --git a/src/legacy/core_plugins/vis_type_table/public/index.ts b/src/legacy/core_plugins/vis_type_table/public/index.ts deleted file mode 100644 index efbaf69659ea2..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/index.ts +++ /dev/null @@ -1,25 +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. - */ - -import { PluginInitializerContext } from '../../../../core/public'; -import { TableVisPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy.ts b/src/legacy/core_plugins/vis_type_table/public/legacy.ts deleted file mode 100644 index 3d5f8c1b3efe9..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/legacy.ts +++ /dev/null @@ -1,34 +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. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from './legacy_imports'; -import { plugin } from '.'; - -import { TablePluginSetupDependencies } from './plugin'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, { data: npStart.plugins.data }); diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts deleted file mode 100644 index 1030e971d6450..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts +++ /dev/null @@ -1,20 +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. - */ - -export { npSetup, npStart } from 'ui/new_platform'; diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/_index.scss b/src/legacy/core_plugins/vis_type_table/public/paginated_table/_index.scss deleted file mode 100644 index 9473b847d3c2b..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/paginated_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './table_cell_filter'; diff --git a/src/legacy/core_plugins/vis_type_table/public/plugin.ts b/src/legacy/core_plugins/vis_type_table/public/plugin.ts deleted file mode 100644 index ea12a5320a14d..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/plugin.ts +++ /dev/null @@ -1,60 +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. - */ -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; - -import { createTableVisFn } from './table_vis_fn'; -import { tableVisTypeDefinition } from './table_vis_type'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { setFormatService } from './services'; - -/** @internal */ -export interface TablePluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; -} - -/** @internal */ -export interface TablePluginStartDependencies { - data: DataPublicPluginStart; -} - -/** @internal */ -export class TableVisPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup( - core: CoreSetup, - { expressions, visualizations }: TablePluginSetupDependencies - ) { - expressions.registerFunction(createTableVisFn); - - visualizations.createBaseVisualization(tableVisTypeDefinition); - } - - public start(core: CoreStart, { data }: TablePluginStartDependencies) { - setFormatService(data.fieldFormats); - } -} diff --git a/src/legacy/core_plugins/vis_type_table/public/services.ts b/src/legacy/core_plugins/vis_type_table/public/services.ts deleted file mode 100644 index b4b491ac7a555..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/services.ts +++ /dev/null @@ -1,25 +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. - */ - -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('table data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts deleted file mode 100644 index 43816121bc23b..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts +++ /dev/null @@ -1,98 +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. - */ - -import { i18n } from '@kbn/i18n'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; -import { Vis } from '../../../../plugins/visualizations/public'; -import { tableVisResponseHandler } from './table_vis_response_handler'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; -import { TableOptions } from './components/table_vis_options'; -import { TableVisualizationController } from './vis_controller'; - -export const tableVisTypeDefinition = { - type: 'table', - name: 'table', - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data Table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display values in a table', - }), - visualization: TableVisualizationController, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', - }, - template: tableVisTemplate, - }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true, - }, - }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], - }, - ]), - }, - responseHandler: tableVisResponseHandler, - hierarchicalData: (vis: Vis) => { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - }, -}; diff --git a/src/legacy/core_plugins/vis_type_table/public/types.ts b/src/legacy/core_plugins/vis_type_table/public/types.ts deleted file mode 100644 index c6de14b9f050c..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/types.ts +++ /dev/null @@ -1,48 +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. - */ - -import { SchemaConfig } from '../../../../plugins/visualizations/public'; - -export enum AggTypes { - SUM = 'sum', - AVG = 'avg', - MIN = 'min', - MAX = 'max', - COUNT = 'count', -} - -export interface Dimensions { - buckets: SchemaConfig[]; - metrics: SchemaConfig[]; -} - -export interface TableVisParams { - type: 'table'; - perPage: number | ''; - showPartialRows: boolean; - showMetricsAtAllLevels: boolean; - sort: { - columnIndex: number | null; - direction: string | null; - }; - showTotal: boolean; - totalFunc: AggTypes; - percentageCol: string; - dimensions: Dimensions; -} diff --git a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts deleted file mode 100644 index 5bb730d2f9b10..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts +++ /dev/null @@ -1,104 +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. - */ - -import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; - -import { VisParams, ExprVis } from '../../../../plugins/visualizations/public'; -import { npStart } from './legacy_imports'; -import { getAngularModule } from './get_inner_angular'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; - -const innerAngularName = 'kibana/table_vis'; - -export class TableVisualizationController { - private tableVisModule: IModule | undefined; - private injector: auto.IInjectorService | undefined; - el: JQuery; - vis: ExprVis; - $rootScope: IRootScopeService | null = null; - $scope: (IScope & { [key: string]: any }) | undefined; - $compile: ICompileService | undefined; - - constructor(domeElement: Element, vis: ExprVis) { - this.el = $(domeElement); - this.vis = vis; - } - - getInjector() { - if (!this.injector) { - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); - this.injector = angular.bootstrap(mountpoint, [innerAngularName]); - this.el.append(mountpoint); - } - - return this.injector; - } - - initLocalAngular() { - if (!this.tableVisModule) { - this.tableVisModule = getAngularModule(innerAngularName, npStart.core); - initTableVisLegacyModule(this.tableVisModule); - } - } - - async render(esResponse: object, visParams: VisParams) { - this.initLocalAngular(); - - return new Promise(async (resolve, reject) => { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - const updateScope = () => { - if (!this.$scope) { - return; - } - this.$scope.vis = this.vis; - this.$scope.visState = { params: visParams }; - this.$scope.esResponse = esResponse; - - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = this.vis.getUiState(); - updateScope(); - this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); - } - }); - } - - destroy() { - if (this.$rootScope) { - this.$rootScope.$destroy(); - this.$rootScope = null; - } - } -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/index.ts b/src/legacy/core_plugins/vis_type_tagcloud/index.ts deleted file mode 100644 index 6f768131f2190..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/index.ts +++ /dev/null @@ -1,44 +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. - */ - -import { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const tagCloudPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'tagcloud', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default tagCloudPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/package.json b/src/legacy/core_plugins/vis_type_tagcloud/package.json deleted file mode 100644 index 4200ef264fece..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "tagcloud", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss b/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss deleted file mode 100644 index a4fcf8418ce1c..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "tgc" to avoid conflicts. -// Examples -// tgcChart -// tgcChart__legend -// tgcChart__legend--small -// tgcChart__legend-isLoading - -@import './tag_cloud'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts deleted file mode 100644 index 90e6305262caa..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts +++ /dev/null @@ -1,25 +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. - */ - -import { PluginInitializerContext } from '../../../../core/public'; -import { TagCloudPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts deleted file mode 100644 index f70789edc66ba..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts +++ /dev/null @@ -1,34 +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. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { TagCloudPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, { data: npStart.plugins.data }); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts deleted file mode 100644 index 1061271aa315b..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts +++ /dev/null @@ -1,71 +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. - */ - -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; - -import { createTagCloudFn } from './tag_cloud_fn'; -import { createTagCloudVisTypeDefinition } from './tag_cloud_type'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { setFormatService } from './services'; - -/** @internal */ -export interface TagCloudPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - charts: ChartsPluginSetup; -} - -/** @internal */ -export interface TagCloudVisDependencies { - colors: ChartsPluginSetup['colors']; -} - -/** @internal */ -export interface TagCloudVisPluginStartDependencies { - data: DataPublicPluginStart; -} - -/** @internal */ -export class TagCloudPlugin implements Plugin { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public setup( - core: CoreSetup, - { expressions, visualizations, charts }: TagCloudPluginSetupDependencies - ) { - const visualizationDependencies: TagCloudVisDependencies = { - colors: charts.colors, - }; - expressions.registerFunction(createTagCloudFn); - visualizations.createBaseVisualization( - createTagCloudVisTypeDefinition(visualizationDependencies) - ); - } - - public start(core: CoreStart, { data }: TagCloudVisPluginStartDependencies) { - setFormatService(data.fieldFormats); - } -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts deleted file mode 100644 index 272bed3e91a08..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts +++ /dev/null @@ -1,27 +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. - */ - -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('data.fieldFormats'); - -export { npStart } from 'ui/new_platform'; diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts deleted file mode 100644 index ac7e407ca9e4d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/index.ts +++ /dev/null @@ -1,55 +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. - */ - -import { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const vegaPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_type_vega', - deprecations: ({ rename }: { rename: any }) => [ - rename('vega.enabled', 'vis_type_vega.enabled'), - ], - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - enableExternalUrls: Joi.boolean().default(false), - }).default(); - }, - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => { - const serverConfig = server.config(); - const mapConfig: Record = serverConfig.get('map'); - - return { - emsTileLayerId: mapConfig.emsTileLayerId, - }; - }, - }, - init: (server: Legacy.Server) => ({}), - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default vegaPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_vega/package.json b/src/legacy/core_plugins/vis_type_vega/package.json deleted file mode 100644 index acd6f3da128ab..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "author": "Yuri Astrakhan", - "name": "vega", - "version": "kibana" -} - diff --git a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts deleted file mode 100644 index b2f3e5b2241e6..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts +++ /dev/null @@ -1,55 +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. - */ - -import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../plugins/data/public'; -import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; -import { coreMock } from '../../../../../core/public/mocks'; - -export const [getData, setData] = createGetterSetter('Data'); -setData(dataPluginMock.createStartContract()); - -export const [getNotifications, setNotifications] = createGetterSetter( - 'Notifications' -); -setNotifications(coreMock.createStart().notifications); - -export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); -setUISettings(coreMock.createStart().uiSettings); - -export const [getSavedObjects, setSavedObjects] = createGetterSetter( - 'SavedObjects' -); -setSavedObjects(coreMock.createStart().savedObjects); - -export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ - esShardTimeout: number; - enableExternalUrls: boolean; - emsTileLayerId: unknown; -}>('InjectedVars'); -setInjectedVars({ - emsTileLayerId: {}, - enableExternalUrls: true, - esShardTimeout: 10000, -}); - -export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; -export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; -export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.scss b/src/legacy/core_plugins/vis_type_vega/public/index.scss deleted file mode 100644 index 1ab2119d481a0..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/index.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "vga" to avoid conflicts. -// Examples -// vgaChart -// vgaChart__legend -// vgaChart__legend--small -// vgaChart__legend-isLoading - -@import './vega_vis'; -@import './vega_editor'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.ts b/src/legacy/core_plugins/vis_type_vega/public/index.ts deleted file mode 100644 index 34ca0e72190e4..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/index.ts +++ /dev/null @@ -1,25 +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. - */ - -import { PluginInitializerContext } from '../../../../core/public'; -import { VegaPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts deleted file mode 100644 index 450af4a6f253e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ /dev/null @@ -1,38 +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. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { VegaPluginSetupDependencies, VegaPluginStartDependencies } from './plugin'; -import { plugin } from '.'; - -const setupPlugins: Readonly = { - ...npSetup.plugins, - visualizations: npSetup.plugins.visualizations, - mapsLegacy: npSetup.plugins.mapsLegacy, -}; - -const startPlugins: Readonly = { - ...npStart.plugins, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts deleted file mode 100644 index 9fa77d28fbbfa..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ /dev/null @@ -1,96 +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. - */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; -import { - setNotifications, - setData, - setSavedObjects, - setInjectedVars, - setUISettings, -} from './services'; - -import { createVegaFn } from './vega_fn'; -import { createVegaTypeDefinition } from './vega_type'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; -import { IServiceSettings } from '../../../../plugins/maps_legacy/public'; - -/** @internal */ -export interface VegaVisualizationDependencies { - core: CoreSetup; - plugins: { - data: ReturnType; - }; - serviceSettings: IServiceSettings; -} - -/** @internal */ -export interface VegaPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - data: ReturnType; - visTypeVega: VisTypeVegaSetup; - mapsLegacy: any; -} - -/** @internal */ -export interface VegaPluginStartDependencies { - data: ReturnType; -} - -/** @internal */ -export class VegaPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup( - core: CoreSetup, - { data, expressions, visualizations, visTypeVega, mapsLegacy }: VegaPluginSetupDependencies - ) { - setInjectedVars({ - enableExternalUrls: visTypeVega.config.enableExternalUrls, - esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, - emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), - }); - setUISettings(core.uiSettings); - - const visualizationDependencies: Readonly = { - core, - plugins: { - data, - }, - serviceSettings: mapsLegacy.serviceSettings, - }; - - expressions.registerFunction(() => createVegaFn(visualizationDependencies)); - - visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); - } - - public start(core: CoreStart, { data }: VegaPluginStartDependencies) { - setNotifications(core.notifications); - setSavedObjects(core.savedObjects); - setData(data); - } -} diff --git a/src/legacy/core_plugins/vis_type_vega/public/services.ts b/src/legacy/core_plugins/vis_type_vega/public/services.ts deleted file mode 100644 index 88e0e0098bf8c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/services.ts +++ /dev/null @@ -1,46 +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. - */ - -import { SavedObjectsStart } from 'kibana/public'; -import { NotificationsStart } from 'src/core/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { IUiSettingsClient } from '../../../../core/public'; - -export const [getData, setData] = createGetterSetter('Data'); - -export const [getNotifications, setNotifications] = createGetterSetter( - 'Notifications' -); - -export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); - -export const [getSavedObjects, setSavedObjects] = createGetterSetter( - 'SavedObjects' -); - -export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ - esShardTimeout: number; - enableExternalUrls: boolean; - emsTileLayerId: unknown; -}>('InjectedVars'); - -export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; -export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; -export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index ef3f664252856..26800f8a1620e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -108,6 +108,6 @@ export class VisTypeVislibPlugin implements Plugin { public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { setFormatService(data.fieldFormats); - setDataActions({ createFiltersFromEvent: data.actions.createFiltersFromEvent }); + setDataActions(data.actions); } } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index c378ae7b05b37..6bf66c2bdd788 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -36,7 +36,9 @@ jest.mock('../../../legacy_imports', () => ({ })); jest.mock('../../../services', () => ({ - getDataActions: () => ({ createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']) }), + getDataActions: () => ({ + createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), + }), })); const vis = { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 2fe16bbfeb625..7eb25e3930718 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -101,7 +101,7 @@ export class VisLegend extends PureComponent { return false; } - const filters = await getDataActions().createFiltersFromEvent(item.values); + const filters = await getDataActions().createFiltersFromValueClickAction({ data: item.values }); return Boolean(filters.length); }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js index ecf67ee3e017c..f33ce0395af1f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -83,10 +83,21 @@ export class Handler { // memoize so that the same function is returned every time, // allowing us to remove/re-add the same function - this.getProxyHandler = _.memoize(function(event) { + this.getProxyHandler = _.memoize(function(eventType) { const self = this; - return function(e) { - self.vis.emit(event, e); + return function(eventPayload) { + switch (eventType) { + case 'brush': + const xRaw = _.get(eventPayload.data, 'series[0].values[0].xRaw'); + if (!xRaw) return; // not sure if this is possible? + return self.vis.emit(eventType, { + table: xRaw.table, + range: eventPayload.range, + column: xRaw.column, + }); + case 'click': + return self.vis.emit(eventType, eventPayload); + } }; }); diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index aaed52f8b120a..b36e62297cc23 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -9,7 +9,6 @@ // kbnChart__legend-isLoading @import './accessibility/index'; -@import './chrome/index'; @import './directives/index'; @import './error_auto_create_index/index'; @import './error_url_overflow/index'; diff --git a/src/legacy/ui/public/chrome/_index.scss b/src/legacy/ui/public/chrome/_index.scss deleted file mode 100644 index 7e6c3ebaccc5c..0000000000000 --- a/src/legacy/ui/public/chrome/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './variables'; - -@import './directives/index'; diff --git a/src/legacy/ui/public/chrome/_variables.scss b/src/legacy/ui/public/chrome/_variables.scss deleted file mode 100644 index 5097fe4c9bfae..0000000000000 --- a/src/legacy/ui/public/chrome/_variables.scss +++ /dev/null @@ -1,4 +0,0 @@ -$kbnGlobalNavClosedWidth: 53px; -$kbnGlobalNavOpenWidth: 180px; -$kbnGlobalNavLogoHeight: 70px; -$kbnGlobalNavAppIconHeight: $euiSizeXXL + $euiSizeXS; diff --git a/src/legacy/ui/public/chrome/directives/_index.scss b/src/legacy/ui/public/chrome/directives/_index.scss deleted file mode 100644 index 4d00b02279116..0000000000000 --- a/src/legacy/ui/public/chrome/directives/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './kbn_chrome'; diff --git a/src/legacy/ui/public/chrome/directives/_kbn_chrome.scss b/src/legacy/ui/public/chrome/directives/_kbn_chrome.scss deleted file mode 100644 index b29a83848d291..0000000000000 --- a/src/legacy/ui/public/chrome/directives/_kbn_chrome.scss +++ /dev/null @@ -1,46 +0,0 @@ -/** - * stretch the root element of the Kibana application to set the base-size that - * flexed children should keep. Only works when paired with root styles applied - * by core service from new platform - */ -// SASSTODO: Naming here is too embedded and high up that changing them could cause major breaks -#kibana-body { - overflow-x: hidden; - min-height: 100%; -} - -.app-wrapper { - display: flex; - flex-flow: column nowrap; - position: absolute; - left: $kbnGlobalNavClosedWidth; - top: 0; - right: 0; - bottom: 0; - z-index: 5; - margin: 0 auto; - - /** - * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state - * when we're looking at the log-in screen. - */ - &.hidden-chrome { - left: 0 !important; /* 1 */ - } - - .navbar-right { - margin-right: 0; - } -} - -.app-wrapper-panel { - display: flex; - flex-grow: 1; - flex-shrink: 0; - flex-basis: auto; - flex-direction: column; - - > * { - flex-shrink: 0; - } -} diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx index 4d0f5d3a5bd56..c918554563fcb 100644 --- a/src/legacy/ui/public/i18n/index.tsx +++ b/src/legacy/ui/public/i18n/index.tsx @@ -44,7 +44,7 @@ export function wrapInI18nContext

(ComponentToWrap: React.ComponentType

) { } uiModules - .get('i18n') + .get('i18n', ['ngSanitize']) .provider('i18n', I18nProvider) .filter('i18n', i18nFilter) .directive('i18nId', i18nDirective); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f14f26613ef01..271586bb8c582 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -377,7 +377,8 @@ export const npStart = { }, data: { actions: { - createFiltersFromEvent: Promise.resolve(['yes']), + createFiltersFromValueClickAction: Promise.resolve(['yes']), + createFiltersFromRangeSelectAction: sinon.fake(), }, autocomplete: { getProvider: sinon.fake(), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 5ae2e2348aaa1..a15c7cce5511d 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -59,7 +59,6 @@ import { NavigationPublicPluginSetup, NavigationPublicPluginStart, } from '../../../../plugins/navigation/public'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; import { DiscoverSetup, DiscoverStart } from '../../../../plugins/discover/public'; import { SavedObjectsManagementPluginSetup, @@ -88,7 +87,6 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; - visTypeVega: VisTypeVegaSetup; discover: DiscoverSetup; visualizations: VisualizationsSetup; telemetry?: TelemetryPluginSetup; diff --git a/src/legacy/ui/ui_bundles/app_entry_template.js b/src/legacy/ui/ui_bundles/app_entry_template.js index a1c3a153a196c..683fedd34316f 100644 --- a/src/legacy/ui/ui_bundles/app_entry_template.js +++ b/src/legacy/ui/ui_bundles/app_entry_template.js @@ -25,6 +25,9 @@ export const appEntryTemplate = bundle => ` * * This is programmatically created and updated, do not modify * + * Any changes to this file should be kept in sync with + * src/core/public/entry_point.ts + * * context: ${bundle.getContext()} */ @@ -45,7 +48,9 @@ i18n.load(injectedMetadata.i18n.translationsUrl) browserSupportsCsp: !window.__kbnCspNotEnforced__, requireLegacyFiles: () => { ${bundle.getRequires().join('\n ')} - } + }, + requireLegacyBootstrapModule: () => require('ui/chrome'), + requireNewPlatformShimModule: () => require('ui/new_platform'), }); coreSystem diff --git a/src/legacy/ui/ui_bundles/ui_bundles_controller.js b/src/legacy/ui/ui_bundles/ui_bundles_controller.js index 7afa283af83e0..79112fd687e84 100644 --- a/src/legacy/ui/ui_bundles/ui_bundles_controller.js +++ b/src/legacy/ui/ui_bundles/ui_bundles_controller.js @@ -99,13 +99,6 @@ export class UiBundlesController { this._postLoaders = []; this._bundles = []; - // create a bundle for core-only with no modules - this.add({ - id: 'core', - modules: [], - template: appEntryTemplate, - }); - // create a bundle for each uiApp for (const uiApp of uiApps) { this.add({ diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index bb246d97bfe4e..35e1f8b7d2127 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -24,6 +24,7 @@ export const UI_EXPORT_DEFAULTS = { webpackNoParseRules: [ /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, /node_modules[\/\\](mocha|moment)[\/\\]/, + /node_modules[\/\\]vega-lib[\/\\]build[\/\\]vega\.js$/, ], webpackAliases: { diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 1093153edbbf7..8a71c6ccb1506 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,6 +1,7 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; window.__kbnDarkMode__ = {{darkMode}}; +window.__kbnPublicPath__ = {{publicPathMap}}; if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); @@ -69,26 +70,16 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { } load([ - {{#each sharedJsDepFilenames}} - '{{../regularBundlePath}}/kbn-ui-shared-deps/{{this}}', - {{/each}} - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js', - {{#each dllJsChunks}} + {{#each jsDependencyPaths}} '{{this}}', {{/each}} - '{{regularBundlePath}}/commons.bundle.js', - {{!-- '{{regularBundlePath}}/plugin/data/data.plugin.js', --}} - '{{regularBundlePath}}/plugin/kibanaUtils/kibanaUtils.plugin.js', - '{{regularBundlePath}}/plugin/esUiShared/esUiShared.plugin.js', - '{{regularBundlePath}}/plugin/kibanaReact/kibanaReact.plugin.js' ], function () { load([ - '{{regularBundlePath}}/{{appId}}.bundle.js', + '{{entryBundlePath}}', {{#each styleSheetPaths}} '{{this}}', {{/each}} - ]) + ]); }); - }; + } } diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 0912d8683fc48..801eecf5b608b 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -103,41 +103,78 @@ export function uiRenderMixin(kbnServer, server, config) { const dllJsChunks = DllCompiler.getRawDllConfig().chunks.map( chunk => `${dllBundlePath}/vendors${chunk}.bundle.dll.js` ); + const styleSheetPaths = [ - ...dllStyleChunks, + ...(isCore ? [] : dllStyleChunks), `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, ...(darkMode ? [ `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, + `${regularBundlePath}/dark_theme.style.css`, ] : [ `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, + `${regularBundlePath}/light_theme.style.css`, ]), - `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, `${regularBundlePath}/commons.style.css`, - ...(!isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : []), - ...kbnServer.uiExports.styleSheetPaths - .filter(path => path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light')) - .map(path => - path.localPath.endsWith('.scss') - ? `${basePath}/built_assets/css/${path.publicPath}` - : `${basePath}/${path.publicPath}` - ) - .reverse(), + ...(isCore + ? [] + : [ + `${regularBundlePath}/${app.getId()}.style.css`, + ...kbnServer.uiExports.styleSheetPaths + .filter( + path => path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light') + ) + .map(path => + path.localPath.endsWith('.scss') + ? `${basePath}/built_assets/css/${path.publicPath}` + : `${basePath}/${path.publicPath}` + ) + .reverse(), + ]), ]; + const jsDependencyPaths = [ + ...UiSharedDeps.jsDepFilenames.map( + filename => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` + ), + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + ...(isCore + ? [] + : [ + `${dllBundlePath}/vendors_runtime.bundle.dll.js`, + ...dllJsChunks, + `${regularBundlePath}/commons.bundle.js`, + ]), + `${regularBundlePath}/plugin/kibanaUtils/kibanaUtils.plugin.js`, + `${regularBundlePath}/plugin/esUiShared/esUiShared.plugin.js`, + `${regularBundlePath}/plugin/kibanaReact/kibanaReact.plugin.js`, + ]; + + const uiPluginIds = [...kbnServer.newPlatform.__internals.uiPlugins.public.keys()]; + + // These paths should align with the bundle routes configured in + // src/optimize/bundles_route/bundles_route.js + const publicPathMap = JSON.stringify({ + core: `${regularBundlePath}/core/`, + 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, + ...uiPluginIds.reduce( + (acc, pluginId) => ({ ...acc, [pluginId]: `${regularBundlePath}/plugin/${pluginId}/` }), + {} + ), + }); + const bootstrap = new AppBootstrap({ templateData: { - appId: isCore ? 'core' : app.getId(), - regularBundlePath, - dllBundlePath, - dllJsChunks, - styleSheetPaths, - sharedJsFilename: UiSharedDeps.jsFilename, - sharedJsDepFilenames: UiSharedDeps.jsDepFilenames, darkMode, + jsDependencyPaths, + styleSheetPaths, + publicPathMap, + entryBundlePath: isCore + ? `${regularBundlePath}/core/core.entry.js` + : `${regularBundlePath}/${app.getId()}.bundle.js`, }, }); diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js index 0c2e98b5acd63..4030988c8552c 100644 --- a/src/optimize/bundles_route/bundles_route.js +++ b/src/optimize/bundles_route/bundles_route.js @@ -17,11 +17,12 @@ * under the License. */ -import { isAbsolute, extname } from 'path'; +import { isAbsolute, extname, join } from 'path'; import LruCache from 'lru-cache'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { createDynamicAssetResponse } from './dynamic_asset_response'; import { assertIsNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; +import { fromRoot } from '../../core/server/utils'; /** * Creates the routes that serves files from `bundlesPath` or from @@ -71,37 +72,57 @@ export function createBundlesRoute({ } return [ - buildRouteForBundles( - `${basePublicPath}/bundles/kbn-ui-shared-deps/`, - '/bundles/kbn-ui-shared-deps/', - UiSharedDeps.distDir, - fileHashCache - ), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`, + routePath: '/bundles/kbn-ui-shared-deps/', + bundlesPath: UiSharedDeps.distDir, + fileHashCache, + replacePublicPath: false, + }), ...npUiPluginPublicDirs.map(({ id, path }) => - buildRouteForBundles( - `${basePublicPath}/bundles/plugin/${id}/`, - `/bundles/plugin/${id}/`, - path, - fileHashCache - ) - ), - buildRouteForBundles( - `${basePublicPath}/bundles/`, - '/bundles/', - regularBundlesPath, - fileHashCache + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/plugin/${id}/`, + routePath: `/bundles/plugin/${id}/`, + bundlesPath: path, + fileHashCache, + replacePublicPath: false, + }) ), - buildRouteForBundles( - `${basePublicPath}/built_assets/dlls/`, - '/built_assets/dlls/', - dllBundlesPath, - fileHashCache - ), - buildRouteForBundles(`${basePublicPath}/`, '/built_assets/css/', builtCssPath, fileHashCache), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/core/`, + routePath: `/bundles/core/`, + bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), + fileHashCache, + replacePublicPath: false, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/`, + routePath: '/bundles/', + bundlesPath: regularBundlesPath, + fileHashCache, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/built_assets/dlls/`, + routePath: '/built_assets/dlls/', + bundlesPath: dllBundlesPath, + fileHashCache, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/`, + routePath: '/built_assets/css/', + bundlesPath: builtCssPath, + fileHashCache, + }), ]; } -function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) { +function buildRouteForBundles({ + publicPath, + routePath, + bundlesPath, + fileHashCache, + replacePublicPath = true, +}) { return { method: 'GET', path: `${routePath}{path*}`, @@ -122,6 +143,7 @@ function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) bundlesPath, fileHashCache, publicPath, + replacePublicPath, }); }, }, diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.js index 7af780a79e430..80c49a26270fd 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.js +++ b/src/optimize/bundles_route/dynamic_asset_response.js @@ -52,7 +52,7 @@ import { replacePlaceholder } from '../public_path_placeholder'; * @property {LruCache} options.fileHashCache */ export async function createDynamicAssetResponse(options) { - const { request, h, bundlesPath, publicPath, fileHashCache } = options; + const { request, h, bundlesPath, publicPath, fileHashCache, replacePublicPath } = options; let fd; try { @@ -78,11 +78,14 @@ export async function createDynamicAssetResponse(options) { }); fd = null; // read stream is now responsible for fd + const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; + const etag = replacePublicPath ? `${hash}-${publicPath}` : hash; + return h - .response(replacePlaceholder(read, publicPath)) + .response(content) .takeover() .code(200) - .etag(`${hash}-${publicPath}`) + .etag(etag) .header('cache-control', 'must-revalidate') .type(request.server.mime.path(path).type); } catch (error) { diff --git a/src/plugins/dashboard/common/bwc/types.ts b/src/plugins/dashboard/common/bwc/types.ts new file mode 100644 index 0000000000000..2427799345463 --- /dev/null +++ b/src/plugins/dashboard/common/bwc/types.ts @@ -0,0 +1,149 @@ +/* + * 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. + */ + +import { SavedObjectReference } from 'kibana/public'; + +import { GridData } from '../'; + +interface SavedObjectAttributes { + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; +} + +interface Doc { + references: SavedObjectReference[]; + attributes: Attributes; + id: string; + type: string; +} + +interface DocPre700 { + attributes: Attributes; + id: string; + type: string; +} + +interface DashboardAttributes extends SavedObjectAttributes { + panelsJSON: string; + description: string; + version: number; + timeRestore: boolean; + useMargins?: boolean; + title: string; + optionsJSON?: string; +} + +interface DashboardAttributesTo720 extends SavedObjectAttributes { + panelsJSON: string; + description: string; + uiStateJSON: string; + version: number; + timeRestore: boolean; + useMargins?: boolean; + title: string; + optionsJSON?: string; +} + +export type DashboardDoc730ToLatest = Doc; + +export type DashboardDoc700To720 = Doc; + +export type DashboardDocPre700 = DocPre700; + +// Note that these types are prefixed with `Raw` because there are some post processing steps +// that happen before the saved objects even reach the client. Namely, injecting type and id +// parameters back into the panels, where the raw saved objects actually have them stored elsewhere. +// +// Ideally, everywhere in the dashboard code would use references at the top level instead of +// embedded in the panels. The reason this is stored at the top level is so the references can be uniformly +// updated across all saved object types that have references. + +// Starting in 7.3 we introduced the possibility of embeddables existing without an id +// parameter. If there was no id, then type remains on the panel. So it either will have a name, +// or a type property. +export type RawSavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + // Should be either type, and not name (not backed by a saved object), or name but not type (backed by a + // saved object and type and id are stored on references). Had trouble with oring the two types + // because of optional properties being marked as required: https://github.com/microsoft/TypeScript/issues/20722 + readonly type?: string; + readonly name?: string; + + panelIndex: string; +}; + +// NOTE!! +// All of these types can actually exist in 7.2! The names are pretty confusing because we did +// in place migrations for so long. For example, `RawSavedDashboardPanelTo60` is what a panel +// created in 6.0 will look like after it's been migrated up to 7.2, *not* what it would look like in 6.0. +// That's why it actually doesn't have id or type, but has a name property, because that was a migration +// added in 7.0. + +// Hopefully since we finally have a formal saved object migration system and we can do less in place +// migrations, this will be easier to understand moving forward. + +// Starting in 6.4 we added an in-place edit on panels to remove columns and sort properties and put them +// inside the embeddable config (https://github.com/elastic/kibana/pull/17446). +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in +// this shape in v 7.2. +export type RawSavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel630, + Exclude +>; + +// In 6.3.0 we expanded the number of grid columns and rows: https://github.com/elastic/kibana/pull/16763 +// We added in-place migrations to multiply older x,y,h,w numbers. Note the typescript shape here is the same +// because it's just multiplying existing fields. +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in 7.2 +// that need to be modified. +export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620; + +// In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property. +// Source: https://github.com/elastic/kibana/pull/14949 +export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & { + embeddableConfig: { [key: string]: unknown }; + version: string; +}; + +// In 6.1 we switched from an angular grid to react grid layout (https://github.com/elastic/kibana/pull/13853) +// This used gridData instead of size_x, size_y, row and col. We also started tracking the version this panel +// was created in to make future migrations easier. +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in +// this shape in v 7.2. +export type RawSavedDashboardPanel610 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { gridData: GridData; version: string }; + +export interface RawSavedDashboardPanelTo60 { + readonly columns?: string[]; + readonly sort?: string; + readonly size_x?: number; + readonly size_y?: number; + readonly row: number; + readonly col: number; + panelIndex?: number | string; // earlier versions allowed this to be number or string. Some very early versions seem to be missing this entirely + readonly name: string; + + // This is where custom panel titles are stored prior to Embeddable API v2 + title?: string; +} diff --git a/src/plugins/dashboard/common/embeddable/types.ts b/src/plugins/dashboard/common/embeddable/types.ts new file mode 100644 index 0000000000000..eb76d73af7a58 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/types.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +export interface GridData { + w: number; + h: number; + x: number; + y: number; + i: string; +} diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts new file mode 100644 index 0000000000000..e3f3f629ae5d0 --- /dev/null +++ b/src/plugins/dashboard/common/index.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +export { GridData } from './embeddable/types'; +export { + RawSavedDashboardPanel730ToLatest, + DashboardDoc730ToLatest, + DashboardDoc700To720, + DashboardDocPre700, +} from './bwc/types'; +export { + SavedDashboardPanelTo60, + SavedDashboardPanel610, + SavedDashboardPanel620, + SavedDashboardPanel630, + SavedDashboardPanel640To720, + SavedDashboardPanel730ToLatest, +} from './types'; + +export { migratePanelsTo730 } from './migrate_to_730_panels'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts b/src/plugins/dashboard/common/migrate_to_730_panels.test.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts rename to src/plugins/dashboard/common/migrate_to_730_panels.test.ts index 4dd71fd8ee5f4..0867909225ddb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts +++ b/src/plugins/dashboard/common/migrate_to_730_panels.test.ts @@ -19,15 +19,12 @@ import { migratePanelsTo730 } from './migrate_to_730_panels'; import { RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, RawSavedDashboardPanel630, RawSavedDashboardPanel640To720, - DEFAULT_PANEL_WIDTH, - DEFAULT_PANEL_HEIGHT, - SavedDashboardPanelTo60, - SavedDashboardPanel730ToLatest, -} from '../../../../../../plugins/dashboard/public'; + RawSavedDashboardPanel610, + RawSavedDashboardPanel620, +} from './bwc/types'; +import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from './types'; test('6.0 migrates uiState, sort, scales, and gridData', async () => { const uiState = { @@ -96,8 +93,8 @@ test('6.0 migration gives default width and height when missing', () => { }, ]; const newPanels = migratePanelsTo730(panels, '8.0.0', true); - expect(newPanels[0].gridData.w).toBe(DEFAULT_PANEL_WIDTH); - expect(newPanels[0].gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(newPanels[0].gridData.w).toBe(24); + expect(newPanels[0].gridData.h).toBe(15); expect(newPanels[0].version).toBe('8.0.0'); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts b/src/plugins/dashboard/common/migrate_to_730_panels.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts rename to src/plugins/dashboard/common/migrate_to_730_panels.ts index a19c861f092d5..b89345f0a872c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts +++ b/src/plugins/dashboard/common/migrate_to_730_panels.ts @@ -21,17 +21,19 @@ import semver from 'semver'; import uuid from 'uuid'; import { GridData, + SavedDashboardPanelTo60, + SavedDashboardPanel620, + SavedDashboardPanel630, + SavedDashboardPanel610, +} from './'; +import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel630, RawSavedDashboardPanel640To720, RawSavedDashboardPanel730ToLatest, RawSavedDashboardPanel610, RawSavedDashboardPanel620, - SavedDashboardPanelTo60, - SavedDashboardPanel620, - SavedDashboardPanel630, - SavedDashboardPanel610, -} from '../../../../../../plugins/dashboard/public'; +} from './bwc/types'; const PANEL_HEIGHT_SCALE_FACTOR = 5; const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4; @@ -92,7 +94,7 @@ function migratePre61PanelToLatest( ): RawSavedDashboardPanel730ToLatest { if (panel.col === undefined || panel.row === undefined) { throw new Error( - i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', { + i18n.translate('dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', { defaultMessage: 'Unable to migrate panel data for "6.1.0" backwards compatibility, panel does not contain expected col and/or row fields', }) @@ -151,7 +153,7 @@ function migrate610PanelToLatest( (['w', 'x', 'h', 'y'] as Array).forEach(key => { if (panel.gridData[key] === undefined) { throw new Error( - i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', { + i18n.translate('dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', { defaultMessage: 'Unable to migrate panel data for "6.3.0" backwards compatibility, panel does not contain expected field: {key}', values: { key }, diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts new file mode 100644 index 0000000000000..7cc82a9173976 --- /dev/null +++ b/src/plugins/dashboard/common/types.ts @@ -0,0 +1,76 @@ +/* + * 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. + */ + +import { + RawSavedDashboardPanelTo60, + RawSavedDashboardPanel610, + RawSavedDashboardPanel620, + RawSavedDashboardPanel630, + RawSavedDashboardPanel640To720, + RawSavedDashboardPanel730ToLatest, +} from './bwc/types'; + +export type SavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel630 = Pick< + RawSavedDashboardPanel630, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel620 = Pick< + RawSavedDashboardPanel620, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel610 = Pick< + RawSavedDashboardPanel610, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanelTo60 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +// id becomes optional starting in 7.3.0 +export type SavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel730ToLatest, + Exclude +> & { + readonly id?: string; + readonly type: string; +}; diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 9bcd999c2dcc0..4cd8f3c7d981f 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -11,6 +11,6 @@ "savedObjects" ], "optionalPlugins": ["home", "share", "usageCollection"], - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 7210879c5eacc..1bc85fa110ca0 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -304,13 +304,13 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` url="/plugins/kibana/home/assets/welcome_graphic_light_2x.png" >

@@ -998,13 +998,13 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] url="/plugins/kibana/home/assets/welcome_graphic_light_2x.png" >
diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index 3134a5bfe2c67..a1696298117b0 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -38,12 +38,7 @@ import { EmbeddableStart } from '../../../embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../navigation/public'; import { DataPublicPluginStart } from '../../../data/public'; import { SharePluginStart } from '../../../share/public'; -import { - KibanaLegacyStart, - configureAppAngularModule, - createTopNavDirective, - createTopNavHelper, -} from '../../../kibana_legacy/public'; +import { KibanaLegacyStart, configureAppAngularModule } from '../../../kibana_legacy/public'; import { SavedObjectLoader } from '../../../saved_objects/public'; export interface RenderDeps { @@ -114,13 +109,11 @@ function mountDashboardApp(appBasePath: string, element: HTMLElement) { function createLocalAngularModule(core: AppMountContext['core'], navigation: NavigationStart) { createLocalI18nModule(); - createLocalTopNavModule(navigation); createLocalIconModule(); const dashboardAngularModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, 'app/dashboard/I18n', - 'app/dashboard/TopNav', 'app/dashboard/icon', ]); return dashboardAngularModule; @@ -132,13 +125,6 @@ function createLocalIconModule() { .directive('icon', reactDirective => reactDirective(EuiIcon)); } -function createLocalTopNavModule(navigation: NavigationStart) { - angular - .module('app/dashboard/TopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); -} - function createLocalI18nModule() { angular .module('app/dashboard/I18n', []) diff --git a/src/plugins/dashboard/public/application/dashboard_app.html b/src/plugins/dashboard/public/application/dashboard_app.html index 3cf8932958b6d..87a5728ac2059 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.html +++ b/src/plugins/dashboard/public/application/dashboard_app.html @@ -2,52 +2,7 @@ class="app-container dshAppContainer" ng-class="{'dshAppContainer--withMargins': model.useMargins}" > - - - - - - - - +

{{screenTitle}}

diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 150cd8f8fcbb5..f101935b9288d 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -33,7 +33,6 @@ import { SavedObjectDashboard } from '../saved_dashboards'; export interface DashboardAppScope extends ng.IScope { dash: SavedObjectDashboard; appState: DashboardAppState; - screenTitle: string; model: { query: Query; filters: Filter[]; @@ -54,21 +53,7 @@ export interface DashboardAppScope extends ng.IScope { getShouldShowEditHelp: () => boolean; getShouldShowViewHelp: () => boolean; updateQueryAndFetch: ({ query, dateRange }: { query: Query; dateRange?: TimeRange }) => void; - onRefreshChange: ({ - isPaused, - refreshInterval, - }: { - isPaused: boolean; - refreshInterval: any; - }) => void; - onFiltersUpdated: (filters: Filter[]) => void; - onCancelApplyFilters: () => void; - onApplyFilters: (filters: Filter[]) => void; - onQuerySaved: (savedQuery: SavedQuery) => void; - onSavedQueryUpdated: (savedQuery: SavedQuery) => void; - onClearSavedQuery: () => void; topNavMenu: any; - showFilterBar: () => boolean; showAddPanel: any; showSaveQuery: boolean; kbnTopNav: any; diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 283fe9f0a83a4..fa2f06bfcdcdd 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -21,12 +21,15 @@ import _, { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import React from 'react'; +import ReactDOM from 'react-dom'; import angular from 'angular'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { History } from 'history'; import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; +import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; +import { TimeRange } from 'src/plugins/data/public'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; import { @@ -87,6 +90,7 @@ export interface DashboardAppControllerDependencies extends RenderDeps { dashboardConfig: KibanaLegacyStart['dashboardConfig']; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; + navigation: NavigationStart; } export class DashboardAppController { @@ -123,10 +127,13 @@ export class DashboardAppController { history, kbnUrlStateStorage, usageCollection, + navigation, }: DashboardAppControllerDependencies) { const filterManager = queryService.filterManager; const queryFilter = filterManager; const timefilter = queryService.timefilter.timefilter; + let showSearchBar = true; + let showQueryBar = true; let lastReloadRequestTime = 0; const dash = ($scope.dash = $route.current.locals.dash); @@ -243,6 +250,9 @@ export class DashboardAppController { } }; + const showFilterBar = () => + $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); + const getEmptyScreenProps = ( shouldShowEditHelp: boolean, isEmptyInReadOnlyMode: boolean @@ -310,7 +320,6 @@ export class DashboardAppController { refreshInterval: timefilter.getRefreshInterval(), }; $scope.panels = dashboardStateManager.getPanels(); - $scope.screenTitle = dashboardStateManager.getTitle(); }; updateState(); @@ -515,49 +524,8 @@ export class DashboardAppController { } }; - $scope.onRefreshChange = function({ isPaused, refreshInterval }) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value, - }); - }; - - $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); - }; - - $scope.onQuerySaved = savedQuery => { - $scope.savedQuery = savedQuery; - }; - - $scope.onSavedQueryUpdated = savedQuery => { - $scope.savedQuery = { ...savedQuery }; - }; - - $scope.onClearSavedQuery = () => { - delete $scope.savedQuery; - dashboardStateManager.setSavedQueryId(undefined); - dashboardStateManager.applyFilters( - { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }, - queryFilter.getGlobalFilters() - ); - // Making this method sync broke the updates. - // Temporary fix, until we fix the complex state in this file. - setTimeout(() => { - queryFilter.setFilters(queryFilter.getGlobalFilters()); - }, 0); - }; - const updateStateFromSavedQuery = (savedQuery: SavedQuery) => { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = queryFilter.getGlobalFilters(); - const allFilters = [...globalFilters, ...savedQueryFilters]; - + const allFilters = filterManager.getFilters(); dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters); if (savedQuery.attributes.timefilter) { timefilter.setTime({ @@ -616,6 +584,48 @@ export class DashboardAppController { } ); + const onSavedQueryIdChange = (savedQueryId?: string) => { + dashboardStateManager.setSavedQueryId(savedQueryId); + }; + + const getNavBarProps = () => { + const isFullScreenMode = dashboardStateManager.getFullScreenMode(); + const screenTitle = dashboardStateManager.getTitle(); + return { + appName: 'dashboard', + config: $scope.isVisible ? $scope.topNavMenu : undefined, + className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined, + screenTitle, + showSearchBar, + showQueryBar, + showFilterBar: showFilterBar(), + indexPatterns: $scope.indexPatterns, + showSaveQuery: $scope.showSaveQuery, + query: $scope.model.query, + savedQuery: $scope.savedQuery, + onSavedQueryIdChange, + savedQueryId: dashboardStateManager.getSavedQueryId(), + useDefaultBehaviors: true, + onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }): void => { + if (!payload.query) { + $scope.updateQueryAndFetch({ query: $scope.model.query, dateRange: payload.dateRange }); + } else { + $scope.updateQueryAndFetch({ query: payload.query, dateRange: payload.dateRange }); + } + }, + }; + }; + const dashboardNavBar = document.getElementById('dashboardChrome'); + const updateNavBar = () => { + ReactDOM.render(, dashboardNavBar); + }; + + const unmountNavBar = () => { + if (dashboardNavBar) { + ReactDOM.unmountComponentAtNode(dashboardNavBar); + } + }; + $scope.timefilterSubscriptions$ = new Subscription(); $scope.timefilterSubscriptions$.add( @@ -707,6 +717,8 @@ export class DashboardAppController { revertChangesAndExitEditMode(); } }); + + updateNavBar(); }; /** @@ -761,9 +773,6 @@ export class DashboardAppController { }); } - $scope.showFilterBar = () => - $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); - $scope.showAddPanel = () => { dashboardStateManager.setFullScreenMode(false); /* @@ -785,7 +794,11 @@ export class DashboardAppController { const navActions: { [key: string]: NavAction; } = {}; - navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true); + navActions[TopNavIds.FULL_SCREEN] = () => { + dashboardStateManager.setFullScreenMode(true); + showQueryBar = false; + updateNavBar(); + }; navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW); navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(ViewMode.EDIT); navActions[TopNavIds.SAVE] = () => { @@ -858,6 +871,7 @@ export class DashboardAppController { if ((response as { error: Error }).error) { dashboardStateManager.setTitle(currentTitle); } + updateNavBar(); return response; }); }; @@ -939,6 +953,9 @@ export class DashboardAppController { const visibleSubscription = chrome.getIsVisible$().subscribe(isVisible => { $scope.$evalAsync(() => { $scope.isVisible = isVisible; + showSearchBar = isVisible || showFilterBar(); + showQueryBar = !dashboardStateManager.getFullScreenMode() && isVisible; + updateNavBar(); }); }); @@ -949,9 +966,17 @@ export class DashboardAppController { navActions, dashboardConfig.getHideWriteControls() ); + updateNavBar(); + }); + + $scope.$watch('indexPatterns', () => { + updateNavBar(); }); $scope.$on('$destroy', () => { + // we have to unmount nav bar manually to make sure all internal subscriptions are unsubscribed + unmountNavBar(); + updateSubscription.unsubscribe(); stopSyncingQueryServiceStateWithUrl(); stopSyncingAppFilters(); diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index b15a813aff903..fb33649093c8d 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -29,9 +29,10 @@ import _ from 'lodash'; import React from 'react'; import { Subscription } from 'rxjs'; import ReactGridLayout, { Layout } from 'react-grid-layout'; +import { GridData } from '../../../../common'; import { ViewMode, EmbeddableChildPanel } from '../../../embeddable_plugin'; import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; -import { DashboardPanelState, GridData } from '../types'; +import { DashboardPanelState } from '../types'; import { withKibana } from '../../../../../kibana_react/public'; import { DashboardContainerInput } from '../dashboard_container'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; diff --git a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts index 70a6c83418587..b95b7f394a27d 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts @@ -18,7 +18,8 @@ */ import { PanelNotFoundError } from '../../../embeddable_plugin'; -import { DashboardPanelState, GridData, DASHBOARD_GRID_COLUMN_COUNT } from '..'; +import { GridData } from '../../../../common'; +import { DashboardPanelState, DASHBOARD_GRID_COLUMN_COUNT } from '..'; export type PanelPlacementMethod = ( args: PlacementArgs diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts index 30a93989649a7..b3ce2f1e57d5f 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts @@ -33,6 +33,10 @@ export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition return false; } + public canCreateNew() { + return false; + } + public async create(initialInput: EmbeddableInput, parent?: IContainer) { return new PlaceholderEmbeddable(initialInput, parent); } diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 6d0221cb10e8b..66cdd22ed6bd4 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -17,18 +17,11 @@ * under the License. */ import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; +import { GridData } from '../../../common'; import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; export type PanelId = string; export type SavedObjectId = string; -export interface GridData { - w: number; - h: number; - x: number; - y: number; - i: string; -} - export interface DashboardPanelState< TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput > extends PanelState { diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 10243dbf2f979..31225530b10b9 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -28,7 +28,6 @@ import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants'; import { createKbnUrlStateStorage, - ensureDefaultIndexPattern, redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, @@ -138,7 +137,7 @@ export function initDashboardApp(app, deps) { }, resolve: { dash: function($route, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, history).then(() => { + return deps.data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; if (title) { @@ -173,7 +172,8 @@ export function initDashboardApp(app, deps) { requireUICapability: 'dashboard.createNew', resolve: { dash: history => - ensureDefaultIndexPattern(deps.core, deps.data, history) + deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get()) .catch( redirectWhenMissing({ @@ -194,7 +194,8 @@ export function initDashboardApp(app, deps) { dash: function($route, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.data, history) + return deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get(id)) .then(savedDashboard => { deps.chrome.recentlyAccessed.add( diff --git a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts index 8f8de3663518a..f4d97578adebf 100644 --- a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts +++ b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts @@ -22,18 +22,16 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { DashboardAppState, SavedDashboardPanel } from '../../types'; import { - DashboardAppState, + migratePanelsTo730, SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest, SavedDashboardPanel610, SavedDashboardPanel630, SavedDashboardPanel640To720, SavedDashboardPanel620, - SavedDashboardPanel, -} from '../../types'; -// should be moved in src/plugins/dashboard/common right after https://github.com/elastic/kibana/pull/61895 is merged -import { migratePanelsTo730 } from '../../../../../legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels'; +} from '../../../common'; /** * Attempts to migrate the state stored in the URL into the latest version of it. diff --git a/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts b/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts index 57c147ffe3588..ee59c68cce451 100644 --- a/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts +++ b/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts @@ -17,17 +17,19 @@ * under the License. */ -import { searchSourceMock } from '../../../../data/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; import { SavedObjectDashboard } from '../../saved_dashboards'; export function getSavedDashboardMock( config?: Partial ): SavedObjectDashboard { + const searchSource = dataPluginMock.createStartContract(); + return { id: '123', title: 'my dashboard', panelsJSON: '[]', - searchSource: searchSourceMock, + searchSource: searchSource.search.searchSource.create(), copyOnSave: false, timeRestore: false, timeTo: 'now', diff --git a/src/plugins/dashboard/public/bwc/index.ts b/src/plugins/dashboard/public/bwc/index.ts deleted file mode 100644 index d8f7b5091eb8f..0000000000000 --- a/src/plugins/dashboard/public/bwc/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './types'; diff --git a/src/plugins/dashboard/public/bwc/types.ts b/src/plugins/dashboard/public/bwc/types.ts deleted file mode 100644 index d5655e525e9bd..0000000000000 --- a/src/plugins/dashboard/public/bwc/types.ts +++ /dev/null @@ -1,156 +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. - */ - -import { SavedObjectReference } from 'kibana/public'; -import { GridData } from '../application'; - -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} - -export interface Doc { - references: SavedObjectReference[]; - attributes: Attributes; - id: string; - type: string; -} - -export interface DocPre700 { - attributes: Attributes; - id: string; - type: string; -} - -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} - -interface DashboardAttributes extends SavedObjectAttributes { - panelsJSON: string; - description: string; - version: number; - timeRestore: boolean; - useMargins?: boolean; - title: string; - optionsJSON?: string; -} - -export type DashboardAttributes730ToLatest = DashboardAttributes; - -interface DashboardAttributesTo720 extends SavedObjectAttributes { - panelsJSON: string; - description: string; - uiStateJSON: string; - version: number; - timeRestore: boolean; - useMargins?: boolean; - title: string; - optionsJSON?: string; -} - -export type DashboardDoc730ToLatest = Doc; - -export type DashboardDoc700To720 = Doc; - -export type DashboardDocPre700 = DocPre700; - -// Note that these types are prefixed with `Raw` because there are some post processing steps -// that happen before the saved objects even reach the client. Namely, injecting type and id -// parameters back into the panels, where the raw saved objects actually have them stored elsewhere. -// -// Ideally, everywhere in the dashboard code would use references at the top level instead of -// embedded in the panels. The reason this is stored at the top level is so the references can be uniformly -// updated across all saved object types that have references. - -// Starting in 7.3 we introduced the possibility of embeddables existing without an id -// parameter. If there was no id, then type remains on the panel. So it either will have a name, -// or a type property. -export type RawSavedDashboardPanel730ToLatest = Pick< - RawSavedDashboardPanel640To720, - Exclude -> & { - // Should be either type, and not name (not backed by a saved object), or name but not type (backed by a - // saved object and type and id are stored on references). Had trouble with oring the two types - // because of optional properties being marked as required: https://github.com/microsoft/TypeScript/issues/20722 - readonly type?: string; - readonly name?: string; - - panelIndex: string; -}; - -// NOTE!! -// All of these types can actually exist in 7.2! The names are pretty confusing because we did -// in place migrations for so long. For example, `RawSavedDashboardPanelTo60` is what a panel -// created in 6.0 will look like after it's been migrated up to 7.2, *not* what it would look like in 6.0. -// That's why it actually doesn't have id or type, but has a name property, because that was a migration -// added in 7.0. - -// Hopefully since we finally have a formal saved object migration system and we can do less in place -// migrations, this will be easier to understand moving forward. - -// Starting in 6.4 we added an in-place edit on panels to remove columns and sort properties and put them -// inside the embeddable config (https://github.com/elastic/kibana/pull/17446). -// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in -// this shape in v 7.2. -export type RawSavedDashboardPanel640To720 = Pick< - RawSavedDashboardPanel630, - Exclude ->; - -// In 6.3.0 we expanded the number of grid columns and rows: https://github.com/elastic/kibana/pull/16763 -// We added in-place migrations to multiply older x,y,h,w numbers. Note the typescript shape here is the same -// because it's just multiplying existing fields. -// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in 7.2 -// that need to be modified. -export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620; - -// In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property. -// Source: https://github.com/elastic/kibana/pull/14949 -export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & { - embeddableConfig: { [key: string]: unknown }; - version: string; -}; - -// In 6.1 we switched from an angular grid to react grid layout (https://github.com/elastic/kibana/pull/13853) -// This used gridData instead of size_x, size_y, row and col. We also started tracking the version this panel -// was created in to make future migrations easier. -// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in -// this shape in v 7.2. -export type RawSavedDashboardPanel610 = Pick< - RawSavedDashboardPanelTo60, - Exclude -> & { gridData: GridData; version: string }; - -export interface RawSavedDashboardPanelTo60 { - readonly columns?: string[]; - readonly sort?: string; - readonly size_x?: number; - readonly size_y?: number; - readonly row: number; - readonly col: number; - panelIndex?: number | string; // earlier versions allowed this to be number or string. Some very early versions seem to be missing this entirely - readonly name: string; - - // This is where custom panel titles are stored prior to Embeddable API v2 - title?: string; -} diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index ca0ea0293b07c..44733499cdcba 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -20,29 +20,6 @@ import { PluginInitializerContext } from '../../../core/public'; import { DashboardPlugin } from './plugin'; -/** - * These types can probably be internal once all of dashboard app is migrated into this plugin. Right - * now, migrations are still in legacy land. - */ -export { - DashboardDoc730ToLatest, - DashboardDoc700To720, - RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, - RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, - DashboardDocPre700, -} from './bwc'; -export { - SavedDashboardPanelTo60, - SavedDashboardPanel610, - SavedDashboardPanel620, - SavedDashboardPanel630, - SavedDashboardPanel730ToLatest, -} from './types'; - export { DashboardContainer, DashboardContainerInput, @@ -51,7 +28,6 @@ export { // Types below here can likely be made private when dashboard app moved into this NP plugin. DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT, - GridData, } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 203c784d9df4e..5f6b67ee6ad20 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -251,6 +251,8 @@ export class DashboardPlugin localStorage: new Storage(localStorage), usageCollection, }; + // make sure the index pattern list is up to date + await dataStart.indexPatterns.clearCache(); const { renderApp } = await import('./application/application'); const unmount = renderApp(params.element, params.appBasePath, deps); return () => { diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index d96d2cdf75626..21c6bbc1bfc51 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,14 +19,7 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { - RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, - RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, -} from './bwc'; +import { SavedDashboardPanel730ToLatest } from '../common'; import { ViewMode } from './embeddable_plugin'; export interface DashboardCapabilities { @@ -83,55 +76,6 @@ export type NavAction = (anchorElement?: any) => void; */ export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; -// id becomes optional starting in 7.3.0 -export type SavedDashboardPanel730ToLatest = Pick< - RawSavedDashboardPanel730ToLatest, - Exclude -> & { - readonly id?: string; - readonly type: string; -}; - -export type SavedDashboardPanel640To720 = Pick< - RawSavedDashboardPanel640To720, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel630 = Pick< - RawSavedDashboardPanel630, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel620 = Pick< - RawSavedDashboardPanel620, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel610 = Pick< - RawSavedDashboardPanel610, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanelTo60 = Pick< - RawSavedDashboardPanelTo60, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts new file mode 100644 index 0000000000000..9719586001c59 --- /dev/null +++ b/src/plugins/dashboard/server/index.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +import { PluginInitializerContext } from '../../../core/server'; +import { DashboardPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new DashboardPlugin(initializerContext); +} + +export { DashboardPluginSetup, DashboardPluginStart } from './types'; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts new file mode 100644 index 0000000000000..5d1b66002e749 --- /dev/null +++ b/src/plugins/dashboard/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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. + */ + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { dashboardSavedObjectType } from './saved_objects'; + +import { DashboardPluginSetup, DashboardPluginStart } from './types'; + +export class DashboardPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('dashboard: Setup'); + + core.savedObjects.registerType(dashboardSavedObjectType); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('dashboard: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts new file mode 100644 index 0000000000000..65d5a4021f962 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ + +import { SavedObjectsType } from 'kibana/server'; +import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; + +export const dashboardSavedObjectType: SavedObjectsType = { + name: 'dashboard', + hidden: false, + namespaceType: 'single', + management: { + icon: 'dashboardApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'dashboard.show', + }; + }, + }, + mappings: { + properties: { + description: { type: 'text' }, + hits: { type: 'integer' }, + kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, + optionsJSON: { type: 'text' }, + panelsJSON: { type: 'text' }, + refreshInterval: { + properties: { + display: { type: 'keyword' }, + pause: { type: 'boolean' }, + section: { type: 'integer' }, + value: { type: 'integer' }, + }, + }, + timeFrom: { type: 'keyword' }, + timeRestore: { type: 'boolean' }, + timeTo: { type: 'keyword' }, + title: { type: 'text' }, + version: { type: 'integer' }, + }, + }, + migrations: dashboardSavedObjectTypeMigrations, +}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts new file mode 100644 index 0000000000000..9829498118cc0 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -0,0 +1,448 @@ +/* + * 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. + */ + +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; + +describe('dashboard', () => { + describe('7.0.0', () => { + const migration = migrations['7.0.0']; + + test('skips error on empty object', () => { + expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` +Object { + "references": Array [], +} +`); + }); + + test('skips errors when searchSourceJSON is null', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: null, + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips errors when searchSourceJSON is undefined', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: undefined, + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when searchSourceJSON is not a string', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: 123, + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when searchSourceJSON is invalid json', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{abc123}', + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true }), + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('extracts "index" attribute from doc', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('extracts index patterns from filter', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + bar: true, + filter: [ + { + meta: { + foo: true, + index: 'my-index', + }, + }, + ], + }), + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when panelsJSON is not a string', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: 123, + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": 123, + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips error when panelsJSON is not valid JSON', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '{123abc}', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "{123abc}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips panelsJSON when its not an array', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '{}', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "{}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips error when a panel is missing "type" attribute', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '[{"id":"123"}]', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "[{\\"id\\":\\"123\\"}]", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips error when a panel is missing "id" attribute', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '[{"type":"visualization"}]', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('extract panel references from doc', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + } as SavedObjectUnsanitizedDoc; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], +} +`); + }); + }); +}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts new file mode 100644 index 0000000000000..7c1d0568cd3d7 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -0,0 +1,117 @@ +/* + * 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. + */ + +import { get, flow } from 'lodash'; + +import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { migrations730 } from './migrations_730'; +import { migrateMatchAllQuery } from './migrate_match_all_query'; +import { DashboardDoc700To720 } from '../../common'; + +function migrateIndexPattern(doc: DashboardDoc700To720) { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return; + } + if (searchSource.index) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: 'index-pattern', + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); +} + +const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { + // Set new "references" attribute + doc.references = doc.references || []; + + // Migrate index pattern + migrateIndexPattern(doc as DashboardDoc700To720); + // Migrate panels + const panelsJSON = get(doc, 'attributes.panelsJSON'); + if (typeof panelsJSON !== 'string') { + return doc as DashboardDoc700To720; + } + let panels; + try { + panels = JSON.parse(panelsJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc as DashboardDoc700To720; + } + if (!Array.isArray(panels)) { + return doc as DashboardDoc700To720; + } + panels.forEach((panel, i) => { + if (!panel.type || !panel.id) { + return; + } + panel.panelRefName = `panel_${i}`; + doc.references!.push({ + name: `panel_${i}`, + type: panel.type, + id: panel.id, + }); + delete panel.type; + delete panel.id; + }); + doc.attributes.panelsJSON = JSON.stringify(panels); + return doc as DashboardDoc700To720; +}; + +export const dashboardSavedObjectTypeMigrations = { + /** + * We need to have this migration twice, once with a version prior to 7.0.0 once with a version + * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already + * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, + * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we + * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects + * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced + * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 + * only contained the 6.7.2 migration and not the 7.0.1 migration. + */ + '6.7.2': flow(migrateMatchAllQuery), + '7.0.0': flow<(doc: SavedObjectUnsanitizedDoc) => DashboardDoc700To720>(migrations700), + '7.3.0': flow(migrations730), +}; diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts new file mode 100644 index 0000000000000..ca97b9d2a6b70 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { dashboardSavedObjectType } from './dashboard'; diff --git a/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts b/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts new file mode 100644 index 0000000000000..c9b35263a549f --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { DashboardDoc730ToLatest } from '../../common'; + +function isDoc( + doc: { [key: string]: unknown } | SavedObjectUnsanitizedDoc +): doc is SavedObjectUnsanitizedDoc { + return ( + typeof doc.id === 'string' && + typeof doc.type === 'string' && + doc.attributes !== null && + typeof doc.attributes === 'object' && + doc.references !== null && + typeof doc.references === 'object' + ); +} + +export function isDashboardDoc( + doc: { [key: string]: unknown } | DashboardDoc730ToLatest +): doc is DashboardDoc730ToLatest { + if (!isDoc(doc)) { + return false; + } + + if (typeof (doc as DashboardDoc730ToLatest).attributes.panelsJSON !== 'string') { + return false; + } + + return true; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.test.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.test.ts rename to src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts similarity index 95% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts rename to src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts index 707aae9e5d4ac..5b8582bf821ef 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts @@ -19,7 +19,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { get } from 'lodash'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../../../../plugins/data/common'; +import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index 5a4970897098d..aa744324428a4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -17,20 +17,18 @@ * under the License. */ -import { migrations } from '../../../migrations'; +import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; -import { - DashboardDoc700To720, - DashboardDoc730ToLatest, - RawSavedDashboardPanel730ToLatest, - DashboardDocPre700, -} from '../../../../../../plugins/dashboard/public'; - -const mockLogger = { - warning: () => {}, - warn: () => {}, - debug: () => {}, - info: () => {}, +import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; +import { RawSavedDashboardPanel730ToLatest } from '../../common'; + +const mockContext = { + log: { + warning: () => {}, + warn: () => {}, + debug: () => {}, + info: () => {}, + }, }; test('dashboard migration 7.3.0 migrates filters to query on search source', () => { @@ -53,7 +51,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source', () '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const newDoc = migrations730(doc, mockLogger); + const newDoc = migrations730(doc, mockContext); expect(newDoc).toMatchInlineSnapshot(` Object { @@ -97,8 +95,8 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when }, }; - const doc700: DashboardDoc700To720 = migrations.dashboard['7.0.0'](doc, mockLogger); - const newDoc = migrations.dashboard['7.3.0'](doc700, mockLogger); + const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); expect(parsedSearchSource.filter.length).toBe(0); @@ -129,8 +127,8 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => { }, }; - const doc700: DashboardDoc700To720 = migrations.dashboard['7.0.0'](doc, mockLogger); - const newDoc = migrations.dashboard['7.3.0'](doc700, mockLogger); + const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); expect(parsedSearchSource.filter.length).toBe(0); @@ -159,7 +157,7 @@ test('dashboard migration 7.3.0 migrates panels', () => { }, }; - const newDoc = migrations730(doc, mockLogger) as DashboardDoc730ToLatest; + const newDoc = migrations730(doc, mockContext) as DashboardDoc730ToLatest; const newPanels = JSON.parse(newDoc.attributes.panelsJSON) as RawSavedDashboardPanel730ToLatest[]; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.ts similarity index 79% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts rename to src/plugins/dashboard/server/saved_objects/migrations_730.ts index 56856f7b21303..e9d483f68a5da 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.ts @@ -16,26 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -// This file should be moved to dashboard/server/ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsMigrationLogger } from 'src/core/server'; + import { inspect } from 'util'; -import { - DashboardDoc730ToLatest, - DashboardDoc700To720, -} from '../../../../../../plugins/dashboard/public'; +import { SavedObjectMigrationContext } from 'kibana/server'; +import { DashboardDoc730ToLatest } from '../../common'; import { isDashboardDoc } from './is_dashboard_doc'; import { moveFiltersToQuery } from './move_filters_to_query'; -import { migratePanelsTo730 } from './migrate_to_730_panels'; +import { migratePanelsTo730, DashboardDoc700To720 } from '../../common'; -export function migrations730( - doc: - | { - [key: string]: unknown; - } - | DashboardDoc700To720, - logger: SavedObjectsMigrationLogger -): DashboardDoc730ToLatest | { [key: string]: unknown } { +export const migrations730 = (doc: DashboardDoc700To720, { log }: SavedObjectMigrationContext) => { if (!isDashboardDoc(doc)) { // NOTE: we should probably throw an error here... but for now following suit and in the // case of errors, just returning the same document. @@ -48,7 +37,7 @@ export function migrations730( moveFiltersToQuery(searchSource) ); } catch (e) { - logger.warning( + log.warning( `Exception @ migrations730 while trying to migrate dashboard query filters!\n` + `${e.stack}\n` + `dashboard: ${inspect(doc, false, null)}` @@ -75,7 +64,7 @@ export function migrations730( delete doc.attributes.uiStateJSON; } catch (e) { - logger.warning( + log.warning( `Exception @ migrations730 while trying to migrate dashboard panels!\n` + `Error: ${e.stack}\n` + `dashboard: ${inspect(doc, false, null)}` @@ -84,4 +73,4 @@ export function migrations730( } return doc as DashboardDoc730ToLatest; -} +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts similarity index 96% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts rename to src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts index 621983b1ca8a5..a06f64e0f0c40 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts +++ b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts @@ -17,8 +17,8 @@ * under the License. */ +import { esFilters, Filter } from 'src/plugins/data/public'; import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; -import { esFilters, Filter } from '../../../../../../plugins/data/public'; const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts rename to src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts diff --git a/src/plugins/dashboard/server/types.ts b/src/plugins/dashboard/server/types.ts new file mode 100644 index 0000000000000..1151b06dbdab7 --- /dev/null +++ b/src/plugins/dashboard/server/types.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DashboardPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DashboardPluginStart {} diff --git a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js index f745f01873bae..fc6b706f6e01e 100644 --- a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js +++ b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js @@ -222,9 +222,8 @@ module.exports = (function() { if (sequence === 'true') return buildLiteralNode(true); if (sequence === 'false') return buildLiteralNode(false); if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - const number = Number(sequence); - const value = isNaN(number) ? sequence : number; - return buildLiteralNode(value); + const isNumberPattern = /^(-?[1-9]+\d*([.]\d+)?)$|^(-?0[.]\d*[1-9]+)$|^0$|^0.0$|^[.]\d{1,}$/ + return buildLiteralNode(isNumberPattern.test(sequence) ? Number(sequence) : sequence); }, peg$c50 = { type: "any", description: "any character" }, peg$c51 = "*", @@ -3164,4 +3163,4 @@ module.exports = (function() { SyntaxError: peg$SyntaxError, parse: peg$parse }; -})(); \ No newline at end of file +})(); diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts index e441420760475..6a69d52d72134 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts +++ b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts @@ -278,6 +278,33 @@ describe('kuery AST API', () => { expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); expect(fromLiteralExpression('42')).toEqual(numberLiteral); + + expect(fromLiteralExpression('.3').value).toEqual(0.3); + expect(fromLiteralExpression('.36').value).toEqual(0.36); + expect(fromLiteralExpression('.00001').value).toEqual(0.00001); + expect(fromLiteralExpression('3').value).toEqual(3); + expect(fromLiteralExpression('-4').value).toEqual(-4); + expect(fromLiteralExpression('0').value).toEqual(0); + expect(fromLiteralExpression('0.0').value).toEqual(0); + expect(fromLiteralExpression('2.0').value).toEqual(2.0); + expect(fromLiteralExpression('0.8').value).toEqual(0.8); + expect(fromLiteralExpression('790.9').value).toEqual(790.9); + expect(fromLiteralExpression('0.0001').value).toEqual(0.0001); + expect(fromLiteralExpression('96565646732345').value).toEqual(96565646732345); + + expect(fromLiteralExpression('..4').value).toEqual('..4'); + expect(fromLiteralExpression('.3text').value).toEqual('.3text'); + expect(fromLiteralExpression('text').value).toEqual('text'); + expect(fromLiteralExpression('.').value).toEqual('.'); + expect(fromLiteralExpression('-').value).toEqual('-'); + expect(fromLiteralExpression('001').value).toEqual('001'); + expect(fromLiteralExpression('00.2').value).toEqual('00.2'); + expect(fromLiteralExpression('0.0.1').value).toEqual('0.0.1'); + expect(fromLiteralExpression('3.').value).toEqual('3.'); + expect(fromLiteralExpression('--4').value).toEqual('--4'); + expect(fromLiteralExpression('-.4').value).toEqual('-.4'); + expect(fromLiteralExpression('-0').value).toEqual('-0'); + expect(fromLiteralExpression('00949').value).toEqual('00949'); }); test('should allow escaping of special characters with a backslash', () => { diff --git a/src/plugins/data/common/es_query/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg index 389b9a82d2c76..625c5069f936a 100644 --- a/src/plugins/data/common/es_query/kuery/ast/kuery.peg +++ b/src/plugins/data/common/es_query/kuery/ast/kuery.peg @@ -247,9 +247,8 @@ UnquotedLiteral if (sequence === 'true') return buildLiteralNode(true); if (sequence === 'false') return buildLiteralNode(false); if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - const number = Number(sequence); - const value = isNaN(number) ? sequence : number; - return buildLiteralNode(value); + const isNumberPattern = /^(-?[1-9]+\d*([.]\d+)?)$|^(-?0[.]\d*[1-9]+)$|^0$|^0.0$|^[.]\d{1,}$/ + return buildLiteralNode(isNumberPattern.test(sequence) ? Number(sequence) : sequence); } UnquotedCharacter diff --git a/src/plugins/data/common/field_formats/constants/base_formatters.ts b/src/plugins/data/common/field_formats/constants/base_formatters.ts index 6befe8cea71f5..921c50571f727 100644 --- a/src/plugins/data/common/field_formats/constants/base_formatters.ts +++ b/src/plugins/data/common/field_formats/constants/base_formatters.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IFieldFormatType } from '../types'; +import { FieldFormatInstanceType } from '../types'; import { BoolFormat, @@ -36,7 +36,7 @@ import { UrlFormat, } from '../converters'; -export const baseFormatters: IFieldFormatType[] = [ +export const baseFormatters: FieldFormatInstanceType[] = [ BoolFormat, BytesFormat, ColorFormat, diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index a1ce0cf3e7b54..4dd011a7feff3 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -18,9 +18,9 @@ */ import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, FIELD_FORMAT_IDS, IFieldFormatType } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS, FieldFormatInstanceType } from '../types'; -export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): FieldFormatInstanceType => class CustomFieldFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.CUSTOM; diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 49baa8c074da8..96d0024dff2a2 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -22,7 +22,7 @@ import { createCustomFieldFormat } from './converters/custom'; import { FieldFormatsGetConfigFn, FieldFormatsContentType, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatConvert, FieldFormatConvertFunction, HtmlContextTypeOptions, @@ -199,7 +199,7 @@ export abstract class FieldFormat { }; } - static from(convertFn: FieldFormatConvertFunction): IFieldFormatType { + static from(convertFn: FieldFormatConvertFunction): FieldFormatInstanceType { return createCustomFieldFormat(convertFn); } diff --git a/src/plugins/data/common/field_formats/field_formats_registry.test.ts b/src/plugins/data/common/field_formats/field_formats_registry.test.ts index 0b32a62744fb1..f04524505a711 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.test.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.test.ts @@ -18,7 +18,7 @@ */ import { FieldFormatsRegistry } from './field_formats_registry'; import { BoolFormat, PercentFormat, StringFormat } from './converters'; -import { FieldFormatsGetConfigFn, IFieldFormatType } from './types'; +import { FieldFormatsGetConfigFn, FieldFormatInstanceType } from './types'; import { KBN_FIELD_TYPES } from '../../common'; const getValueOfPrivateField = (instance: any, field: string) => instance[field]; @@ -75,10 +75,10 @@ describe('FieldFormatsRegistry', () => { test('should register field formats', () => { fieldFormatsRegistry.register([StringFormat, BoolFormat]); - const registeredFieldFormatters: Map = getValueOfPrivateField( - fieldFormatsRegistry, - 'fieldFormats' - ); + const registeredFieldFormatters: Map< + string, + FieldFormatInstanceType + > = getValueOfPrivateField(fieldFormatsRegistry, 'fieldFormats'); expect(registeredFieldFormatters.size).toBe(2); diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 15b1687e22312..b0a57ad6912a7 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -24,7 +24,7 @@ import { FieldFormatsGetConfigFn, FieldFormatConfig, FIELD_FORMAT_IDS, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatId, IFieldFormatMetaParams, IFieldFormat, @@ -35,7 +35,7 @@ import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types'; export class FieldFormatsRegistry { - protected fieldFormats: Map = new Map(); + protected fieldFormats: Map = new Map(); protected defaultMap: Record = {}; protected metaParamsOptions: Record = {}; protected getConfig?: FieldFormatsGetConfigFn; @@ -47,7 +47,7 @@ export class FieldFormatsRegistry { init( getConfig: FieldFormatsGetConfigFn, metaParamsOptions: Record = {}, - defaultFieldConverters: IFieldFormatType[] = baseFormatters + defaultFieldConverters: FieldFormatInstanceType[] = baseFormatters ) { const defaultTypeMap = getConfig('format:defaultTypeMap'); this.register(defaultFieldConverters); @@ -79,23 +79,23 @@ export class FieldFormatsRegistry { * Get a derived FieldFormat class by its id. * * @param {FieldFormatId} formatId - the format id - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ - getType = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getType = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { const fieldFormat = this.fieldFormats.get(formatId); if (fieldFormat) { const decoratedFieldFormat: any = this.fieldFormatMetaParamsDecorator(fieldFormat); if (decoratedFieldFormat) { - return decoratedFieldFormat as IFieldFormatType; + return decoratedFieldFormat as FieldFormatInstanceType; } } return undefined; }; - getTypeWithoutMetaParams = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getTypeWithoutMetaParams = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { return this.fieldFormats.get(formatId); }; @@ -106,12 +106,12 @@ export class FieldFormatsRegistry { * * @param {KBN_FIELD_TYPES} fieldType * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ getDefaultType = ( fieldType: KBN_FIELD_TYPES, esTypes: ES_FIELD_TYPES[] - ): IFieldFormatType | undefined => { + ): FieldFormatInstanceType | undefined => { const config = this.getDefaultConfig(fieldType, esTypes); return this.getType(config.id); @@ -206,14 +206,16 @@ export class FieldFormatsRegistry { * Get filtered list of field formats by format type * * @param {KBN_FIELD_TYPES} fieldType - * @return {IFieldFormatType[]} + * @return {FieldFormatInstanceType[]} */ - getByFieldType(fieldType: KBN_FIELD_TYPES): IFieldFormatType[] { + getByFieldType(fieldType: KBN_FIELD_TYPES): FieldFormatInstanceType[] { return [...this.fieldFormats.values()] - .filter((format: IFieldFormatType) => format && format.fieldType.indexOf(fieldType) !== -1) + .filter( + (format: FieldFormatInstanceType) => format && format.fieldType.indexOf(fieldType) !== -1 + ) .map( - (format: IFieldFormatType) => - this.fieldFormatMetaParamsDecorator(format) as IFieldFormatType + (format: FieldFormatInstanceType) => + this.fieldFormatMetaParamsDecorator(format) as FieldFormatInstanceType ); } @@ -238,7 +240,7 @@ export class FieldFormatsRegistry { }); } - register(fieldFormats: IFieldFormatType[]) { + register(fieldFormats: FieldFormatInstanceType[]) { fieldFormats.forEach(fieldFormat => this.fieldFormats.set(fieldFormat.id, fieldFormat)); } @@ -246,12 +248,12 @@ export class FieldFormatsRegistry { * FieldFormat decorator - provide a one way to add meta-params for all field formatters * * @private - * @param {IFieldFormatType} fieldFormat - field format type - * @return {IFieldFormatType | undefined} + * @param {FieldFormatInstanceType} fieldFormat - field format type + * @return {FieldFormatInstanceType | undefined} */ private fieldFormatMetaParamsDecorator = ( - fieldFormat: IFieldFormatType - ): IFieldFormatType | undefined => { + fieldFormat: FieldFormatInstanceType + ): FieldFormatInstanceType | undefined => { const getMetaParams = (customParams: Record) => this.buildMetaParams(customParams); if (fieldFormat) { diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index 13d3d9d73d43a..b64e115fd55ff 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -52,6 +52,6 @@ export { FieldFormatConfig, FieldFormatId, // Used in data plugin only - IFieldFormatType, + FieldFormatInstanceType, IFieldFormat, } from './types'; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 7c1d6a8522e52..5f11c7fe094bc 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - import { FieldFormat } from './field_format'; -export { FieldFormat }; /** @public **/ export type FieldFormatsContentType = 'html' | 'text'; @@ -82,10 +80,12 @@ export type IFieldFormat = PublicMethodsOf; */ export type FieldFormatId = FIELD_FORMAT_IDS | string; -export type IFieldFormatType = (new ( +/** @internal **/ +export type FieldFormatInstanceType = (new ( params?: any, getConfig?: FieldFormatsGetConfigFn ) => FieldFormat) & { + // Static properties: id: FieldFormatId; title: string; fieldType: string | string[]; diff --git a/src/plugins/data/public/actions/filters/brush_event.test.ts b/src/plugins/data/public/actions/filters/brush_event.test.ts deleted file mode 100644 index 60244354f06e4..0000000000000 --- a/src/plugins/data/public/actions/filters/brush_event.test.ts +++ /dev/null @@ -1,204 +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. - */ - -import moment from 'moment'; - -import { onBrushEvent, BrushEvent } from './brush_event'; - -import { IndexPatternsContract } from '../../../public'; -import { dataPluginMock } from '../../../public/mocks'; -import { setIndexPatterns } from '../../../public/services'; -import { mockDataServices } from '../../../public/search/aggs/test_helpers'; - -describe('brushEvent', () => { - const DAY_IN_MS = 24 * 60 * 60 * 1000; - const JAN_01_2014 = 1388559600000; - let baseEvent: BrushEvent; - - const aggConfigs = [ - { - params: { - field: {}, - }, - getIndexPattern: () => ({ - timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, - }), - }, - ]; - - beforeEach(() => { - mockDataServices(); - setIndexPatterns(({ - ...dataPluginMock.createStartContract().indexPatterns, - get: async () => ({ - id: 'indexPatternId', - timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, - }), - } as unknown) as IndexPatternsContract); - - baseEvent = { - data: { - ordered: { - date: false, - }, - series: [ - { - values: [ - { - xRaw: { - column: 0, - table: { - columns: [ - { - id: '1', - meta: { - type: 'histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: aggConfigs[0].params, - }, - }, - ], - }, - }, - }, - ], - }, - ], - }, - range: [], - }; - }); - - test('should be a function', () => { - expect(typeof onBrushEvent).toBe('function'); - }); - - test('ignores event when data.xAxisField not provided', async () => { - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); - }); - - describe('handles an event when the x-axis field is a date field', () => { - describe('date field is index pattern timefield', () => { - beforeEach(() => { - aggConfigs[0].params.field = { - name: 'time', - type: 'date', - }; - baseEvent.data.ordered = { date: true }; - }); - - afterAll(() => { - baseEvent.range = []; - baseEvent.data.ordered = { date: false }; - }); - - test('by ignoring the event when range spans zero time', async () => { - baseEvent.range = [JAN_01_2014, JAN_01_2014]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); - }); - - test('by updating the timefilter', async () => { - baseEvent.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeDefined(); - - if (filter) { - expect(filter.range.time.gte).toBe(new Date(JAN_01_2014).toISOString()); - // Set to a baseline timezone for comparison. - expect(filter.range.time.lt).toBe(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); - } - }); - }); - - describe('date field is not index pattern timefield', () => { - beforeEach(() => { - aggConfigs[0].params.field = { - name: 'anotherTimeField', - type: 'date', - }; - baseEvent.data.ordered = { date: true }; - }); - - afterAll(() => { - baseEvent.range = []; - baseEvent.data.ordered = { date: false }; - }); - - test('creates a new range filter', async () => { - const rangeBegin = JAN_01_2014; - const rangeEnd = rangeBegin + DAY_IN_MS; - baseEvent.range = [rangeBegin, rangeEnd]; - const filter = await onBrushEvent(baseEvent); - - expect(filter).toBeDefined(); - - if (filter) { - expect(filter.range.anotherTimeField.gte).toBe(moment(rangeBegin).toISOString()); - expect(filter.range.anotherTimeField.lt).toBe(moment(rangeEnd).toISOString()); - expect(filter.range.anotherTimeField).toHaveProperty( - 'format', - 'strict_date_optional_time' - ); - } - }); - }); - }); - - describe('handles an event when the x-axis field is a number', () => { - beforeAll(() => { - aggConfigs[0].params.field = { - name: 'numberField', - type: 'number', - }; - }); - - afterAll(() => { - baseEvent.range = []; - }); - - test('by ignoring the event when range does not span at least 2 values', async () => { - baseEvent.range = [1]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); - }); - - test('by creating a new filter', async () => { - baseEvent.range = [1, 2, 3, 4]; - const filter = await onBrushEvent(baseEvent); - - expect(filter).toBeDefined(); - - if (filter) { - expect(filter.range.numberField.gte).toBe(1); - expect(filter.range.numberField.lt).toBe(4); - expect(filter.range.numberField).not.toHaveProperty('format'); - } - }); - }); -}); diff --git a/src/plugins/data/public/actions/filters/brush_event.ts b/src/plugins/data/public/actions/filters/brush_event.ts deleted file mode 100644 index 714f005fbeb6d..0000000000000 --- a/src/plugins/data/public/actions/filters/brush_event.ts +++ /dev/null @@ -1,78 +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. - */ - -import { get, last } from 'lodash'; -import moment from 'moment'; -import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; -import { deserializeAggConfig } from '../../search/expressions/utils'; - -export interface BrushEvent { - data: { - ordered: { - date: boolean; - }; - series: Array>; - }; - range: number[]; -} - -export async function onBrushEvent(event: BrushEvent) { - const isDate = get(event.data, 'ordered.date'); - const xRaw: Record = get(event.data, 'series[0].values[0].xRaw'); - - if (!xRaw) { - return; - } - - const column: Record = xRaw.table.columns[xRaw.column]; - - if (!column || !column.meta) { - return; - } - - const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); - const aggConfig = deserializeAggConfig({ - ...column.meta, - indexPattern, - }); - const field: IFieldType = aggConfig.params.field; - - if (!field || event.range.length <= 1) { - return; - } - - const min = event.range[0]; - const max = last(event.range); - - if (min === max) { - return; - } - - const range: RangeFilterParams = { - gte: isDate ? moment(min).toISOString() : min, - lt: isDate ? moment(max).toISOString() : max, - }; - - if (isDate) { - range.format = 'strict_date_optional_time'; - } - - return esFilters.buildRangeFilter(field, range, indexPattern); -} diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts deleted file mode 100644 index 1ed09002816d1..0000000000000 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts +++ /dev/null @@ -1,116 +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. - */ - -import { - fieldFormats, - FieldFormatsGetConfigFn, - esFilters, - IndexPatternsContract, -} from '../../../public'; -import { dataPluginMock } from '../../../public/mocks'; -import { setIndexPatterns } from '../../../public/services'; -import { mockDataServices } from '../../../public/search/aggs/test_helpers'; -import { createFiltersFromEvent, EventData } from './create_filters_from_event'; - -const mockField = { - name: 'bytes', - indexPattern: { - id: 'logstash-*', - }, - filterable: true, - format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), -}; - -describe('createFiltersFromEvent', () => { - let dataPoints: EventData[]; - - beforeEach(() => { - dataPoints = [ - { - table: { - columns: [ - { - name: 'test', - id: '1-1', - meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field: 'bytes', - interval: 30, - otherBucket: true, - }, - }, - }, - ], - rows: [ - { - '1-1': '2048', - }, - ], - }, - column: 0, - row: 0, - value: 'test', - }, - ]; - - mockDataServices(); - setIndexPatterns(({ - ...dataPluginMock.createStartContract().indexPatterns, - get: async () => ({ - id: 'logstash-*', - fields: { - getByName: () => mockField, - filter: () => [mockField], - }, - }), - } as unknown) as IndexPatternsContract); - }); - - test('ignores event when value for rows is not provided', async () => { - dataPoints[0].table.rows[0]['1-1'] = null; - const filters = await createFiltersFromEvent(dataPoints); - - expect(filters.length).toEqual(0); - }); - - test('handles an event when aggregations type is a terms', async () => { - if (dataPoints[0].table.columns[0].meta) { - dataPoints[0].table.columns[0].meta.type = 'terms'; - } - const filters = await createFiltersFromEvent(dataPoints); - - expect(filters.length).toEqual(1); - expect(filters[0].query.match_phrase.bytes).toEqual('2048'); - }); - - test('handles an event when aggregations type is not terms', async () => { - const filters = await createFiltersFromEvent(dataPoints); - - expect(filters.length).toEqual(1); - - const [rangeFilter] = filters; - - if (esFilters.isRangeFilter(rangeFilter)) { - expect(rangeFilter.range.bytes.gte).toEqual(2048); - expect(rangeFilter.range.bytes.lt).toEqual(2078); - } - }); -}); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.ts b/src/plugins/data/public/actions/filters/create_filters_from_event.ts deleted file mode 100644 index e62945a592072..0000000000000 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.ts +++ /dev/null @@ -1,137 +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. - */ - -import { KibanaDatatable } from '../../../../../plugins/expressions/public'; -import { deserializeAggConfig } from '../../search/expressions'; -import { esFilters, Filter } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; - -export interface EventData { - table: Pick; - column: number; - row: number; - value: any; -} - -/** - * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter - * terms based on a specific cell in the tabified data. - * - * @param {EventData['table']} table - tabified table data - * @param {number} columnIndex - current column index - * @param {number} rowIndex - current row index - * @return {array} - array of terms to filter against - */ -const getOtherBucketFilterTerms = ( - table: EventData['table'], - columnIndex: number, - rowIndex: number -) => { - if (rowIndex === -1) { - return []; - } - - // get only rows where cell value matches current row for all the fields before columnIndex - const rows = table.rows.filter(row => { - return table.columns.every((column, i) => { - return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; - }); - }); - const terms: any[] = rows.map(row => row[table.columns[columnIndex].id]); - - return [ - ...new Set( - terms.filter(term => { - const notOther = term !== '__other__'; - const notMissing = term !== '__missing__'; - return notOther && notMissing; - }) - ), - ]; -}; - -/** - * Assembles the filters needed to apply filtering against a specific cell value, while accounting - * for cases like if the value is a terms agg in an `__other__` or `__missing__` bucket. - * - * @param {EventData['table']} table - tabified table data - * @param {number} columnIndex - current column index - * @param {number} rowIndex - current row index - * @param {string} cellValue - value of the current cell - * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() - */ -const createFilter = async (table: EventData['table'], columnIndex: number, rowIndex: number) => { - if (!table || !table.columns || !table.columns[columnIndex]) { - return; - } - const column = table.columns[columnIndex]; - if (!column.meta || !column.meta.indexPatternId) { - return; - } - const aggConfig = deserializeAggConfig({ - type: column.meta.type, - aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, - indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), - }); - let filter: Filter[] = []; - const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; - if (value === null || value === undefined || !aggConfig.isFilterable()) { - return; - } - if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { - const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); - filter = aggConfig.createFilter(value, { terms }); - } else { - filter = aggConfig.createFilter(value); - } - - if (!filter) { - return; - } - - if (!Array.isArray(filter)) { - filter = [filter]; - } - - return filter; -}; - -/** @public */ -export const createFiltersFromEvent = async (dataPoints: EventData[], negate?: boolean) => { - const filters: Filter[] = []; - - await Promise.all( - dataPoints - .filter(point => point) - .map(async val => { - const { table, column, row } = val; - const filter: Filter[] = (await createFilter(table, column, row)) || []; - if (filter) { - filter.forEach(f => { - if (negate) { - f = esFilters.toggleFilterNegated(f); - } - filters.push(f); - }); - } - }) - ); - - return filters; -}; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts new file mode 100644 index 0000000000000..5d21b395b994f --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -0,0 +1,190 @@ +/* + * 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. + */ + +import moment from 'moment'; + +import { createFiltersFromRangeSelectAction } from './create_filters_from_range_select'; + +import { IndexPatternsContract, RangeFilter } from '../../../public'; +import { dataPluginMock } from '../../../public/mocks'; +import { setIndexPatterns } from '../../../public/services'; +import { mockDataServices } from '../../../public/search/aggs/test_helpers'; +import { TriggerContextMapping } from '../../../../ui_actions/public'; + +describe('brushEvent', () => { + const DAY_IN_MS = 24 * 60 * 60 * 1000; + const JAN_01_2014 = 1388559600000; + let baseEvent: TriggerContextMapping['SELECT_RANGE_TRIGGER']['data']; + + const indexPattern = { + id: 'indexPatternId', + timeFieldName: 'time', + fields: { + getByName: () => undefined, + filter: () => [], + }, + }; + + const aggConfigs = [ + { + params: { + field: {}, + }, + getIndexPattern: () => indexPattern, + }, + ]; + + beforeEach(() => { + mockDataServices(); + setIndexPatterns(({ + ...dataPluginMock.createStartContract().indexPatterns, + get: async () => indexPattern, + } as unknown) as IndexPatternsContract); + + baseEvent = { + column: 0, + table: { + type: 'kibana_datatable', + columns: [ + { + id: '1', + name: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, + }, + ], + rows: [], + }, + range: [], + }; + }); + + test('should be a function', () => { + expect(typeof createFiltersFromRangeSelectAction).toBe('function'); + }); + + test('ignores event when data.xAxisField not provided', async () => { + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); + }); + + describe('handles an event when the x-axis field is a date field', () => { + describe('date field is index pattern timefield', () => { + beforeEach(() => { + aggConfigs[0].params.field = { + name: 'time', + type: 'date', + }; + }); + + afterAll(() => { + baseEvent.range = []; + aggConfigs[0].params.field = {}; + }); + + test('by ignoring the event when range spans zero time', async () => { + baseEvent.range = [JAN_01_2014, JAN_01_2014]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); + }); + + test('by updating the timefilter', async () => { + baseEvent.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toBeDefined(); + + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.time.gte).toBe(new Date(JAN_01_2014).toISOString()); + // Set to a baseline timezone for comparison. + expect(rangeFilter.range.time.lt).toBe(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); + } + }); + }); + + describe('date field is not index pattern timefield', () => { + beforeEach(() => { + aggConfigs[0].params.field = { + name: 'anotherTimeField', + type: 'date', + }; + }); + + afterAll(() => { + baseEvent.range = []; + aggConfigs[0].params.field = {}; + }); + + test('creates a new range filter', async () => { + const rangeBegin = JAN_01_2014; + const rangeEnd = rangeBegin + DAY_IN_MS; + baseEvent.range = [rangeBegin, rangeEnd]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + + expect(filter).toBeDefined(); + + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.anotherTimeField.gte).toBe(moment(rangeBegin).toISOString()); + expect(rangeFilter.range.anotherTimeField.lt).toBe(moment(rangeEnd).toISOString()); + expect(rangeFilter.range.anotherTimeField).toHaveProperty( + 'format', + 'strict_date_optional_time' + ); + } + }); + }); + }); + + describe('handles an event when the x-axis field is a number', () => { + beforeAll(() => { + aggConfigs[0].params.field = { + name: 'numberField', + type: 'number', + }; + }); + + afterAll(() => { + baseEvent.range = []; + }); + + test('by ignoring the event when range does not span at least 2 values', async () => { + baseEvent.range = [1]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); + }); + + test('by creating a new filter', async () => { + baseEvent.range = [1, 2, 3, 4]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + + expect(filter).toBeDefined(); + + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.numberField.gte).toBe(1); + expect(rangeFilter.range.numberField.lt).toBe(4); + expect(rangeFilter.range.numberField).not.toHaveProperty('format'); + } + }); + }); +}); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts new file mode 100644 index 0000000000000..409614ca9c380 --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ + +import { last } from 'lodash'; +import moment from 'moment'; +import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; +import { getIndexPatterns } from '../../../public/services'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +import { RangeSelectTriggerContext } from '../../../../embeddable/public'; + +export async function createFiltersFromRangeSelectAction(event: RangeSelectTriggerContext['data']) { + const column: Record = event.table.columns[event.column]; + + if (!column || !column.meta) { + return []; + } + + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); + const field: IFieldType = aggConfig.params.field; + + if (!field || event.range.length <= 1) { + return []; + } + + const min = event.range[0]; + const max = last(event.range); + + if (min === max) { + return []; + } + + const isDate = field.type === 'date'; + + const range: RangeFilterParams = { + gte: isDate ? moment(min).toISOString() : min, + lt: isDate ? moment(max).toISOString() : max, + }; + + if (isDate) { + range.format = 'strict_date_optional_time'; + } + + return esFilters.mapAndFlattenFilters([esFilters.buildRangeFilter(field, range, indexPattern)]); +} diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts new file mode 100644 index 0000000000000..a0e285c20d776 --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -0,0 +1,117 @@ +/* + * 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. + */ + +import { + fieldFormats, + FieldFormatsGetConfigFn, + esFilters, + IndexPatternsContract, +} from '../../../public'; +import { dataPluginMock } from '../../../public/mocks'; +import { setIndexPatterns } from '../../../public/services'; +import { mockDataServices } from '../../../public/search/aggs/test_helpers'; +import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; +import { ValueClickTriggerContext } from '../../../../embeddable/public'; + +const mockField = { + name: 'bytes', + indexPattern: { + id: 'logstash-*', + }, + filterable: true, + format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), +}; + +describe('createFiltersFromValueClick', () => { + let dataPoints: ValueClickTriggerContext['data']['data']; + + beforeEach(() => { + dataPoints = [ + { + table: { + columns: [ + { + name: 'test', + id: '1-1', + meta: { + type: 'histogram', + indexPatternId: 'logstash-*', + aggConfigParams: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, + }, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', + }, + ]; + + mockDataServices(); + setIndexPatterns(({ + ...dataPluginMock.createStartContract().indexPatterns, + get: async () => ({ + id: 'logstash-*', + fields: { + getByName: () => mockField, + filter: () => [mockField], + }, + }), + } as unknown) as IndexPatternsContract); + }); + + test('ignores event when value for rows is not provided', async () => { + dataPoints[0].table.rows[0]['1-1'] = null; + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + + expect(filters.length).toEqual(0); + }); + + test('handles an event when aggregations type is a terms', async () => { + if (dataPoints[0].table.columns[0].meta) { + dataPoints[0].table.columns[0].meta.type = 'terms'; + } + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + + expect(filters.length).toEqual(1); + expect(filters[0].query.match_phrase.bytes).toEqual('2048'); + }); + + test('handles an event when aggregations type is not terms', async () => { + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + + expect(filters.length).toEqual(1); + + const [rangeFilter] = filters; + + if (esFilters.isRangeFilter(rangeFilter)) { + expect(rangeFilter.range.bytes.gte).toEqual(2048); + expect(rangeFilter.range.bytes.lt).toEqual(2078); + } + }); +}); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts new file mode 100644 index 0000000000000..2b426813a98a4 --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -0,0 +1,138 @@ +/* + * 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. + */ + +import { KibanaDatatable } from '../../../../../plugins/expressions/public'; +import { deserializeAggConfig } from '../../search/expressions'; +import { esFilters, Filter } from '../../../public'; +import { getIndexPatterns } from '../../../public/services'; +import { ValueClickTriggerContext } from '../../../../embeddable/public'; + +/** + * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter + * terms based on a specific cell in the tabified data. + * + * @param {EventData['table']} table - tabified table data + * @param {number} columnIndex - current column index + * @param {number} rowIndex - current row index + * @return {array} - array of terms to filter against + */ +const getOtherBucketFilterTerms = ( + table: Pick, + columnIndex: number, + rowIndex: number +) => { + if (rowIndex === -1) { + return []; + } + + // get only rows where cell value matches current row for all the fields before columnIndex + const rows = table.rows.filter(row => { + return table.columns.every((column, i) => { + return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; + }); + }); + const terms: any[] = rows.map(row => row[table.columns[columnIndex].id]); + + return [ + ...new Set( + terms.filter(term => { + const notOther = term !== '__other__'; + const notMissing = term !== '__missing__'; + return notOther && notMissing; + }) + ), + ]; +}; + +/** + * Assembles the filters needed to apply filtering against a specific cell value, while accounting + * for cases like if the value is a terms agg in an `__other__` or `__missing__` bucket. + * + * @param {EventData['table']} table - tabified table data + * @param {number} columnIndex - current column index + * @param {number} rowIndex - current row index + * @param {string} cellValue - value of the current cell + * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() + */ +const createFilter = async ( + table: Pick, + columnIndex: number, + rowIndex: number +) => { + if (!table || !table.columns || !table.columns[columnIndex]) { + return; + } + const column = table.columns[columnIndex]; + if (!column.meta || !column.meta.indexPatternId) { + return; + } + const aggConfig = deserializeAggConfig({ + type: column.meta.type, + aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, + indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), + }); + let filter: Filter[] = []; + const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; + if (value === null || value === undefined || !aggConfig.isFilterable()) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + + if (!filter) { + return; + } + + if (!Array.isArray(filter)) { + filter = [filter]; + } + + return filter; +}; + +/** @public */ +export const createFiltersFromValueClickAction = async ({ + data, + negate, +}: ValueClickTriggerContext['data']) => { + const filters: Filter[] = []; + + await Promise.all( + data + .filter(point => point) + .map(async val => { + const { table, column, row } = val; + const filter: Filter[] = (await createFilter(table, column, row)) || []; + if (filter) { + filter.forEach(f => { + if (negate) { + f = esFilters.toggleFilterNegated(f); + } + filters.push(f); + }); + } + }) + ); + + return esFilters.mapAndFlattenFilters(filters); +}; diff --git a/src/plugins/data/public/actions/index.ts b/src/plugins/data/public/actions/index.ts index cdb84ff13f25e..ef9014aafe82d 100644 --- a/src/plugins/data/public/actions/index.ts +++ b/src/plugins/data/public/actions/index.ts @@ -18,6 +18,7 @@ */ export { ACTION_GLOBAL_APPLY_FILTER, createFilterAction } from './apply_filter_action'; -export { createFiltersFromEvent } from './filters/create_filters_from_event'; +export { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; +export { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; export { selectRangeAction } from './select_range_action'; export { valueClickAction } from './value_click_action'; diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 6e1f16a09e803..4882e8eafc0d3 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -23,19 +23,17 @@ import { IncompatibleActionError, ActionByType, } from '../../../../plugins/ui_actions/public'; -import { onBrushEvent } from './filters/brush_event'; +import { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; +import { RangeSelectTriggerContext } from '../../../embeddable/public'; import { FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; -export interface SelectRangeActionContext { - data: any; - timeFieldName: string; -} +export type SelectRangeActionContext = RangeSelectTriggerContext; async function isCompatible(context: SelectRangeActionContext) { try { - return Boolean(await onBrushEvent(context.data)); + return Boolean(await createFiltersFromRangeSelectAction(context.data)); } catch { return false; } @@ -48,6 +46,7 @@ export function selectRangeAction( return createAction({ type: ACTION_SELECT_RANGE, id: ACTION_SELECT_RANGE, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', @@ -59,13 +58,7 @@ export function selectRangeAction( throw new IncompatibleActionError(); } - const filter = await onBrushEvent(data); - - if (!filter) { - return; - } - - const selectedFilters = esFilters.mapAndFlattenFilters([filter]); + const selectedFilters = await createFiltersFromRangeSelectAction(data); if (timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 01c32e27da07d..210a58b3f75aa 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -26,21 +26,17 @@ import { } from '../../../../plugins/ui_actions/public'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; -import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; +import { ValueClickTriggerContext } from '../../../embeddable/public'; import { Filter, FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; -export interface ValueClickActionContext { - data: any; - timeFieldName: string; -} +export type ValueClickActionContext = ValueClickTriggerContext; async function isCompatible(context: ValueClickActionContext) { try { - const filters: Filter[] = - (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || - []; + const filters: Filter[] = await createFiltersFromValueClickAction(context.data); return filters.length > 0; } catch { return false; @@ -54,23 +50,23 @@ export function valueClickAction( return createAction({ type: ACTION_VALUE_CLICK, id: ACTION_VALUE_CLICK, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', }); }, isCompatible, - execute: async ({ timeFieldName, data }: ValueClickActionContext) => { - if (!(await isCompatible({ timeFieldName, data }))) { + execute: async (context: ValueClickActionContext) => { + if (!(await isCompatible(context))) { throw new IncompatibleActionError(); } - const filters: Filter[] = - (await createFiltersFromEvent(data.data || [data], data.negate)) || []; + const filters: Filter[] = await createFiltersFromValueClickAction(context.data); - let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); + let selectedFilters = filters; - if (selectedFilters.length > 1) { + if (filters.length > 1) { const indexPatterns = await Promise.all( filters.map(filter => { return getIndexPatterns().get(filter.meta.index!); @@ -102,9 +98,9 @@ export function valueClickAction( selectedFilters = await filterSelectionPromise; } - if (timeFieldName) { + if (context.timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - timeFieldName, + context.timeFieldName, selectedFilters ); filterManager.addFilters(restOfFilters); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index b62b728beca35..e1e2576b2a0e7 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -293,6 +293,7 @@ import { convertIPRangeToString, intervalOptions, // only used in Discover isDateHistogramBucketAggConfig, + isNumberType, isStringType, isType, parentPipelineType, @@ -364,8 +365,6 @@ export { SearchResponse, SearchError, ISearchSource, - SearchSource, - createSearchSource, SearchSourceFields, EsQuerySortValue, SortDirection, @@ -392,6 +391,7 @@ export const search = { InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, isDateHistogramBucketAggConfig, + isNumberType, isStringType, isType, isValidEsInterval, diff --git a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx new file mode 100644 index 0000000000000..6b71739862f62 --- /dev/null +++ b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx @@ -0,0 +1,98 @@ +/* + * 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. + */ + +import { contains } from 'lodash'; +import React from 'react'; +import { History } from 'history'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import { toMountPoint } from '../../../../kibana_react/public'; +import { IndexPatternsContract } from './index_patterns'; + +export type EnsureDefaultIndexPattern = (history: History) => Promise | undefined; + +export const createEnsureDefaultIndexPattern = (core: CoreStart) => { + let bannerId: string; + let timeoutId: NodeJS.Timeout | undefined; + + /** + * Checks whether a default index pattern is set and exists and defines + * one otherwise. + * + * If there are no index patterns, redirect to management page and show + * banner. In this case the promise returned from this function will never + * resolve to wait for the URL change to happen. + */ + return async function ensureDefaultIndexPattern(this: IndexPatternsContract, history: History) { + const patterns = await this.getIds(); + let defaultId = core.uiSettings.get('defaultIndex'); + let defined = !!defaultId; + const exists = contains(patterns, defaultId); + + if (defined && !exists) { + core.uiSettings.remove('defaultIndex'); + defaultId = defined = false; + } + + if (defined) { + return; + } + + // If there is any index pattern created, set the first as default + if (patterns.length >= 1) { + defaultId = patterns[0]; + core.uiSettings.set('defaultIndex', defaultId); + } else { + const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; + const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + // Avoid being hostile to new users who don't have an index pattern setup yet + // give them a friendly info message instead of a terse error message + bannerId = core.overlays.banners.replace( + bannerId, + toMountPoint( + + ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + timeoutId = setTimeout(() => { + core.overlays.banners.remove(bannerId); + timeoutId = undefined; + }, 15000); + + history.push(redirectTarget); + + // return never-resolving promise to stop resolving and wait for the url change + return new Promise(() => {}); + } + }; +}; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index c429431b632bd..cf1f83d0e28cb 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -21,9 +21,9 @@ import { IndexPatternsService } from './index_patterns'; import { SavedObjectsClientContract, - IUiSettingsClient, HttpSetup, SavedObjectsFindResponsePublic, + CoreStart, } from 'kibana/public'; jest.mock('./index_pattern', () => { @@ -61,10 +61,10 @@ describe('IndexPatterns', () => { }) as Promise> ); - const uiSettings = {} as IUiSettingsClient; + const core = {} as CoreStart; const http = {} as HttpSetup; - indexPatterns = new IndexPatternsService(uiSettings, savedObjectsClient, http); + indexPatterns = new IndexPatternsService(core, savedObjectsClient, http); }); test('does cache gets for the same id', async () => { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index acce5ed57683c..b5d66a6aab60a 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -22,11 +22,16 @@ import { SimpleSavedObject, IUiSettingsClient, HttpStart, + CoreStart, } from 'src/core/public'; import { createIndexPatternCache } from './_pattern_cache'; import { IndexPattern } from './index_pattern'; import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_client'; +import { + createEnsureDefaultIndexPattern, + EnsureDefaultIndexPattern, +} from './ensure_default_index_pattern'; const indexPatternCache = createIndexPatternCache(); @@ -37,15 +42,13 @@ export class IndexPatternsService { private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array>> | null; private apiClient: IndexPatternsApiClient; + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; - constructor( - config: IUiSettingsClient, - savedObjectsClient: SavedObjectsClientContract, - http: HttpStart - ) { + constructor(core: CoreStart, savedObjectsClient: SavedObjectsClientContract, http: HttpStart) { this.apiClient = new IndexPatternsApiClient(http); - this.config = config; + this.config = core.uiSettings; this.savedObjectsClient = savedObjectsClient; + this.ensureDefaultIndexPattern = createEnsureDefaultIndexPattern(core); } private async refreshSavedObjectsCache() { diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 2d43cae79ac98..ba1df89c41358 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -45,7 +45,8 @@ const createStartContract = (): Start => { const queryStartMock = queryServiceMock.createStartContract(); return { actions: { - createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), + createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), + createFiltersFromRangeSelectAction: jest.fn(), }, autocomplete: autocompleteMock, search: searchStartMock, @@ -56,6 +57,7 @@ const createStartContract = (): Start => { SearchBar: jest.fn(), }, indexPatterns: ({ + ensureDefaultIndexPattern: jest.fn(), make: () => ({ fieldsFetcher: { fetchForWildcard: jest.fn(), @@ -67,7 +69,7 @@ const createStartContract = (): Start => { }; }; -export { searchSourceMock } from './search/mocks'; +export { createSearchSourceMock } from './search/mocks'; export { getCalculateAutoTimeExpression } from './search/aggs'; export const dataPluginMock = { diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 1723545b32522..f3a88287313a0 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -24,13 +24,13 @@ import { Plugin, PackageInfo, } from 'src/core/public'; -import { Storage, IStorageWrapper } from '../../kibana_utils/public'; +import { Storage, IStorageWrapper, createStartServicesGetter } from '../../kibana_utils/public'; import { DataPublicPluginSetup, DataPublicPluginStart, DataSetupDependencies, DataStartDependencies, - GetInternalStartServicesFn, + InternalStartServices, } from './types'; import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; @@ -48,8 +48,6 @@ import { setQueryService, setSearchService, setUiSettings, - getFieldFormats, - getNotifications, } from './services'; import { createSearchBar } from './ui/search_bar/create_search_bar'; import { esaggs } from './search/expressions'; @@ -58,7 +56,12 @@ import { VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, } from '../../ui_actions/public'; -import { ACTION_GLOBAL_APPLY_FILTER, createFilterAction, createFiltersFromEvent } from './actions'; +import { + ACTION_GLOBAL_APPLY_FILTER, + createFilterAction, + createFiltersFromValueClickAction, + createFiltersFromRangeSelectAction, +} from './actions'; import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action'; import { selectRangeAction, @@ -99,15 +102,21 @@ export class DataPublicPlugin implements Plugin { + const { core: coreStart, self }: any = startServices(); + return { + fieldFormats: self.fieldFormats, + notifications: coreStart.notifications, + uiSettings: coreStart.uiSettings, + searchService: self.search, + injectedMetadata: coreStart.injectedMetadata, + }; + }; expressions.registerFunction(esaggs); - const getInternalStartServices: GetInternalStartServicesFn = () => ({ - fieldFormats: getFieldFormats(), - notifications: getNotifications(), - }); - const queryService = this.queryService.setup({ uiSettings: core.uiSettings, storage: this.storage, @@ -130,6 +139,7 @@ export class DataPublicPlugin implements Plugin; - // (undocumented) - schema?: string; - // (undocumented) +export type AggConfigOptions = Assign; // Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -112,7 +105,7 @@ export class AggParamType extends Ba // (undocumented) allowedAggs: string[]; // (undocumented) - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; } // Warning: (ae-missing-release-tag) "AggTypeFieldFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -145,7 +138,7 @@ export class AggTypeFilters { // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[]; +export const baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[]; // Warning: (ae-missing-release-tag) "BUCKET_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -210,9 +203,6 @@ export const connectToQueryState: ({ timefilter: { timefil // @public (undocumented) export const createSavedQueryService: (savedObjectsClient: Pick) => SavedQueryService; -// @public -export const createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise; - // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -248,7 +238,8 @@ export interface DataPublicPluginSetup { export interface DataPublicPluginStart { // (undocumented) actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; // Warning: (ae-forgotten-export) The symbol "AutocompleteStart" needs to be exported by the entry point index.d.ts // @@ -484,7 +475,7 @@ export type FieldFormatId = FIELD_FORMAT_IDS | string; export const fieldFormats: { FieldFormat: typeof FieldFormat; FieldFormatsRegistry: typeof FieldFormatsRegistry; - serialize: (agg: import("./search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; @@ -636,21 +627,21 @@ export type IAggType = AggType; // Warning: (ae-missing-release-tag) "IDataPluginServices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface IDataPluginServices extends Partial { +export interface IDataPluginServices extends Partial { // (undocumented) appName: string; // (undocumented) data: DataPublicPluginStart; // (undocumented) - http: CoreStart['http']; + http: CoreStart_2['http']; // (undocumented) - notifications: CoreStart['notifications']; + notifications: CoreStart_2['notifications']; // (undocumented) - savedObjects: CoreStart['savedObjects']; + savedObjects: CoreStart_2['savedObjects']; // (undocumented) storage: IStorageWrapper; // (undocumented) - uiSettings: CoreStart['uiSettings']; + uiSettings: CoreStart_2['uiSettings']; } // Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1098,7 +1089,7 @@ export type ISearch = // @public (undocumented) export interface ISearchContext { // (undocumented) - core: CoreStart_2; + core: CoreStart; // (undocumented) getSearchStrategy: (name: T) => TSearchStrategyProvider; } @@ -1116,7 +1107,7 @@ export interface ISearchOptions { signal?: AbortSignal; } -// Warning: (ae-missing-release-tag) "ISearchSource" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-forgotten-export) The symbol "SearchSource" needs to be exported by the entry point index.d.ts // // @public (undocumented) export type ISearchSource = Pick; @@ -1311,7 +1302,7 @@ export class Plugin implements Plugin_2 boolean; isStringType: (agg: import("./search").AggConfig) => boolean; - isType: (type: string) => (agg: import("./search").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("./search").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parentPipelineType: string; @@ -1629,61 +1621,6 @@ export type SearchRequest = any; // @public (undocumented) export type SearchResponse = any; -// Warning: (ae-missing-release-tag) "SearchSource" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class SearchSource { - constructor(fields?: SearchSourceFields); - // (undocumented) - create(): SearchSource; - // (undocumented) - createChild(options?: {}): SearchSource; - // (undocumented) - createCopy(): SearchSource; - destroy(): void; - fetch(options?: FetchOptions): Promise; - getField(field: K, recurse?: boolean): SearchSourceFields[K]; - // (undocumented) - getFields(): { - type?: string | undefined; - query?: import("../..").Query | undefined; - filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record | Record[] | undefined; - highlight?: any; - highlightAll?: boolean | undefined; - aggs?: any; - from?: number | undefined; - size?: number | undefined; - source?: string | boolean | string[] | undefined; - version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; - index?: import("../..").IndexPattern | undefined; - searchAfter?: import("./types").EsQuerySearchAfter | undefined; - timeout?: string | undefined; - terminate_after?: number | undefined; - }; - // (undocumented) - getId(): string; - getOwnField(field: K): SearchSourceFields[K]; - getParent(): SearchSource | undefined; - // (undocumented) - getSearchRequestBody(): Promise; - // (undocumented) - history: SearchRequest[]; - onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise): void; - serialize(): { - searchSourceJSON: string; - references: SavedObjectReference[]; - }; - // (undocumented) - setField(field: K, value: SearchSourceFields[K]): this; - // (undocumented) - setFields(newFields: SearchSourceFields): this; - // Warning: (ae-forgotten-export) The symbol "SearchSourceOptions" needs to be exported by the entry point index.d.ts - setParent(parent?: ISearchSource, options?: SearchSourceOptions): this; - setPreferredSearchStrategyId(searchStrategyId: string): void; -} - // Warning: (ae-missing-release-tag) "SearchSourceFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1874,16 +1811,16 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:381:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts @@ -1891,8 +1828,9 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:60:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:61:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/aggs/agg_config.test.ts b/src/plugins/data/public/search/aggs/agg_config.test.ts index 2813e3b9c5373..b5df90313230c 100644 --- a/src/plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/plugins/data/public/search/aggs/agg_config.test.ts @@ -24,18 +24,21 @@ import { AggConfigs, CreateAggConfigParams } from './agg_configs'; import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { MetricAggType } from './metrics/metric_agg_type'; import { Field as IndexPatternField, IndexPattern } from '../../index_patterns'; import { stubIndexPatternWithFields } from '../../../public/stubs'; +import { FieldFormatsStart } from '../../field_formats'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('AggConfig', () => { let indexPattern: IndexPattern; let typesRegistry: AggTypesRegistryStart; - const fieldFormats = fieldFormatsServiceMock.createStartContract(); + let fieldFormats: FieldFormatsStart; beforeEach(() => { jest.restoreAllMocks(); mockDataServices(); + fieldFormats = fieldFormatsServiceMock.createStartContract(); indexPattern = stubIndexPatternWithFields as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); @@ -325,7 +328,7 @@ describe('AggConfig', () => { }); }); - describe('#toJSON', () => { + describe('#serialize', () => { it('includes the aggs id, params, type and schema', () => { const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { @@ -342,7 +345,7 @@ describe('AggConfig', () => { expect(aggConfig.type).toHaveProperty('name', 'date_histogram'); expect(typeof aggConfig.schema).toBe('string'); - const state = aggConfig.toJSON(); + const state = aggConfig.serialize(); expect(state).toHaveProperty('id', '1'); expect(typeof state.params).toBe('object'); expect(state).toHaveProperty('type', 'date_histogram'); @@ -367,6 +370,201 @@ describe('AggConfig', () => { }); }); + describe('#toExpressionAst', () => { + beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; + indexPattern.fields.getByName = name => + ({ + format: { + getConverterFor: (t?: string) => t || identity, + }, + } as IndexPatternField); + }); + + it('works with primitive param types', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'machine.os.keyword', + order: 'asc', + }, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "segment", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + } + `); + }); + + it('creates a subexpression for params of type "agg"', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + orderAgg: { + enabled: true, + type: 'terms', + params: { + field: 'bytes', + order: 'asc', + size: 5, + }, + }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const aggArg = aggConfig.toExpressionAst()?.arguments.orderAgg; + expect(aggArg).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1-orderAgg", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "orderAgg", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => { + // Overwrite the `ranges` param in the `range` agg with a mock toExpressionAst function + const range: MetricAggType = typesRegistry.get('range'); + range.expressionName = 'aggRange'; + const rangesParam = range.params.find(p => p.name === 'ranges'); + rangesParam!.toExpressionAst = (val: any) => ({ + type: 'function', + function: 'aggRanges', + arguments: { + ranges: ['oh hi there!'], + }, + }); + + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'range', + params: { + field: 'bytes', + }, + }; + + const aggConfig = ac.createAggConfig(configStates); + const ranges = aggConfig.toExpressionAst()!.arguments.ranges; + expect(ranges).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "ranges": Array [ + "oh hi there!", + ], + }, + "function": "aggRanges", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('stringifies any other params which are an object', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + json: { foo: 'bar' }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const json = aggConfig.toExpressionAst()?.arguments.json; + expect(json).toEqual([JSON.stringify(configStates.params.json)]); + }); + + it(`returns undefined if an expressionName doesn't exist on the agg type`, () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'unknown type', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toBe(undefined); + }); + }); + describe('#makeLabel', () => { let aggConfig: AggConfig; @@ -422,6 +620,9 @@ describe('AggConfig', () => { let aggConfig: AggConfig; beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; indexPattern.fields.getByName = name => ({ format: { @@ -434,11 +635,7 @@ describe('AggConfig', () => { type: 'histogram', schema: 'bucket', params: { - field: { - format: { - getConverterFor: (t?: string) => t || identity, - }, - }, + field: 'bytes', }, }; const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry, fieldFormats }); @@ -446,6 +643,11 @@ describe('AggConfig', () => { }); it("returns the field's formatter", () => { + aggConfig.params.field = { + format: { + getConverterFor: (t?: string) => t || identity, + }, + }; expect(aggConfig.fieldFormatter().toString()).toBe( aggConfig .getField() diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index 6188849e0e6d4..973c69e3d4f5f 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -19,6 +19,8 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; @@ -27,11 +29,17 @@ import { ISearchSource } from '../search_source'; import { FieldFormatsContentType, KBN_FIELD_TYPES } from '../../../common'; import { FieldFormatsStart } from '../../field_formats'; -export interface AggConfigOptions { - type: IAggType; +type State = string | number | boolean | null | undefined | SerializableState; + +interface SerializableState { + [key: string]: State | State[]; +} + +export interface AggConfigSerialized { + type: string; enabled?: boolean; id?: string; - params?: Record; + params?: SerializableState; schema?: string; } @@ -39,6 +47,8 @@ export interface AggConfigDependencies { fieldFormats: FieldFormatsStart; } +export type AggConfigOptions = Assign; + /** * @name AggConfig * @@ -257,7 +267,10 @@ export class AggConfig { return configDsl; } - toJSON() { + /** + * @returns Returns a serialized representation of an AggConfig. + */ + serialize(): AggConfigSerialized { const params = this.params; const outParams = _.transform( @@ -281,7 +294,64 @@ export class AggConfig { enabled: this.enabled, type: this.type && this.type.name, schema: this.schema, - params: outParams, + params: outParams as SerializableState, + }; + } + + /** + * @deprecated - Use serialize() instead. + */ + toJSON(): AggConfigSerialized { + return this.serialize(); + } + + /** + * @returns Returns an ExpressionAst representing the function for this agg type. + */ + toExpressionAst(): ExpressionAstFunction | undefined { + const functionName = this.type && this.type.expressionName; + const { type, ...rest } = this.serialize(); + if (!functionName || !rest.params) { + // Return undefined - there is no matching expression function for this agg + return; + } + + // Go through each of the params and convert to an array of expression args. + const params = Object.entries(rest.params).reduce((acc, [key, value]) => { + const deserializedParam = this.getAggParams().find(p => p.name === key); + + if (deserializedParam && deserializedParam.toExpressionAst) { + // If the param provides `toExpressionAst`, we call it with the value + const paramExpressionAst = deserializedParam.toExpressionAst(this.getParam(key)); + if (paramExpressionAst) { + acc[key] = [ + { + type: 'expression', + chain: [paramExpressionAst], + }, + ]; + } + } else if (typeof value === 'object') { + // For object params which don't provide `toExpressionAst`, we stringify + acc[key] = [JSON.stringify(value)]; + } else if (typeof value !== 'undefined') { + // Everything else just gets stored in an array if it is defined + acc[key] = [value]; + } + + return acc; + }, {} as Record); + + return { + type: 'function', + function: functionName, + arguments: { + ...params, + // Expression args which are provided to all functions + id: [this.id], + enabled: [this.enabled], + ...(this.schema ? { schema: [this.schema] } : {}), // schema may be undefined + }, }; } diff --git a/src/plugins/data/public/search/aggs/agg_configs.ts b/src/plugins/data/public/search/aggs/agg_configs.ts index 5ad09f824d3e4..d2151a2c5ed4d 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { Assign } from '@kbn/utility-types'; -import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; +import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; @@ -51,7 +51,7 @@ export interface AggConfigsOptions { fieldFormats: FieldFormatsStart; } -export type CreateAggConfigParams = Assign; +export type CreateAggConfigParams = Assign; /** * @name AggConfigs diff --git a/src/plugins/data/public/search/aggs/agg_params.test.ts b/src/plugins/data/public/search/aggs/agg_params.test.ts index 784be803e2644..e116bdca157ff 100644 --- a/src/plugins/data/public/search/aggs/agg_params.test.ts +++ b/src/plugins/data/public/search/aggs/agg_params.test.ts @@ -25,13 +25,15 @@ import { AggParamType } from '../aggs/param_types/agg'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../src/core/public/mocks'; import { AggTypeDependencies } from './agg_type'; +import { InternalStartServices } from '../../types'; describe('AggParams class', () => { const aggTypesDependencies: AggTypeDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; describe('constructor args', () => { diff --git a/src/plugins/data/public/search/aggs/agg_type.test.ts b/src/plugins/data/public/search/aggs/agg_type.test.ts index 0c9e110c34ae6..369ae0ce0b3a5 100644 --- a/src/plugins/data/public/search/aggs/agg_type.test.ts +++ b/src/plugins/data/public/search/aggs/agg_type.test.ts @@ -21,19 +21,21 @@ import { AggType, AggTypeConfig, AggTypeDependencies } from './agg_type'; import { IAggConfig } from './agg_config'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../types'; describe('AggType Class', () => { let dependencies: AggTypeDependencies; beforeEach(() => { dependencies = { - getInternalStartServices: () => ({ - fieldFormats: { - ...fieldFormatsServiceMock.createStartContract(), - getDefaultInstance: jest.fn(() => 'default') as any, - }, - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: { + ...fieldFormatsServiceMock.createStartContract(), + getDefaultInstance: jest.fn(() => 'default') as any, + }, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/public/search/aggs/agg_type.ts index 70c116d560c6f..fb0cb609a08cf 100644 --- a/src/plugins/data/public/search/aggs/agg_type.ts +++ b/src/plugins/data/public/search/aggs/agg_type.ts @@ -39,6 +39,7 @@ export interface AggTypeConfig< createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any; type?: string; dslName?: string; + expressionName?: string; makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string); ordered?: any; hasNoDsl?: boolean; @@ -88,6 +89,14 @@ export class AggType< * @type {string} */ dslName: string; + /** + * the name of the expression function that this aggType represents. + * TODO: this should probably be a required field. + * + * @property name + * @type {string} + */ + expressionName?: string; /** * the user friendly name that will be shown in the ui for this aggType * @@ -219,6 +228,7 @@ export class AggType< this.name = config.name; this.type = config.type || 'metrics'; this.dslName = config.dslName || config.name; + this.expressionName = config.expressionName; this.title = config.title; this.makeLabel = config.makeLabel || constant(this.name); this.ordered = config.ordered; diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts index 4b154c338d48c..da07f581c9274 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/public/search/aggs/agg_types.ts @@ -37,6 +37,7 @@ import { getDerivativeMetricAgg } from './metrics/derivative'; import { getCumulativeSumMetricAgg } from './metrics/cumulative_sum'; import { getMovingAvgMetricAgg } from './metrics/moving_avg'; import { getSerialDiffMetricAgg } from './metrics/serial_diff'; + import { getDateHistogramBucketAgg } from './buckets/date_histogram'; import { getHistogramBucketAgg } from './buckets/histogram'; import { getRangeBucketAgg } from './buckets/range'; @@ -103,3 +104,7 @@ export const getAggTypes = ({ getGeoTitleBucketAgg({ getInternalStartServices }), ], }); + +import { aggTerms } from './buckets/terms_fn'; + +export const getAggTypesFunctions = () => [aggTerms]; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index 7778fcb36bcd6..bb73c8a39df19 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -32,6 +32,7 @@ import { RangeFilter } from '../../../../../common'; import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; import { queryServiceMock } from '../../../../query/mocks'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('date_histogram', () => { @@ -47,10 +48,11 @@ describe('AggConfig Filters', () => { aggTypesDependencies = { uiSettings, query: queryServiceMock.createSetupContract(), - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; mockDataServices(); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts index 4207fa92736f8..0d66d9cfcdca2 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts @@ -28,6 +28,7 @@ import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('Date range', () => { @@ -38,10 +39,11 @@ describe('AggConfig Filters', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts index bf05f7463db6c..0fdb07cc4198a 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts @@ -24,6 +24,7 @@ import { mockAggTypesRegistry } from '../../test_helpers'; import { IBucketAggConfig } from '../bucket_agg_type'; import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('filters', () => { @@ -34,10 +35,11 @@ describe('AggConfig Filters', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts index 1999b759a23d0..72d2029a12b0d 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts @@ -28,6 +28,6 @@ export const createFilterFilters = (aggConfig: IBucketAggConfig, key: string) => const indexPattern = aggConfig.getIndexPattern(); if (filter && indexPattern && indexPattern.id) { - return buildQueryFilter(filter.query, indexPattern.id, key); + return buildQueryFilter(filter, indexPattern.id, key); } }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts index d85576a0ccb14..990adde5f8a0b 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts @@ -26,16 +26,18 @@ import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../core/public/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('IP range', () => { const fieldFormats = fieldFormatsServiceMock.createStartContract(); const typesRegistry = mockAggTypesRegistry([ getIpRangeBucketAgg({ - getInternalStartServices: () => ({ - fieldFormats, - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }), ]); const getAggConfigs = (aggs: CreateAggConfigParams[]) => { diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts index cadd8e9fe13ed..564e7b4763c8d 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts @@ -26,6 +26,7 @@ import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../core/public/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('range', () => { @@ -33,10 +34,11 @@ describe('AggConfig Filters', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; mockDataServices(); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts index d9ff63613b640..36e4bef025ef9 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts @@ -27,6 +27,7 @@ import { Filter, ExistsFilter } from '../../../../../common'; import { RangeBucketAggDependencies } from '../range'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../core/public/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('terms', () => { @@ -34,10 +35,11 @@ describe('AggConfig Filters', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts index f78f0cce732e7..e1881c3bbc7f4 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts @@ -23,6 +23,7 @@ import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; +import { InternalStartServices } from '../../../types'; describe('date_range params', () => { let aggTypesDependencies: DateRangeBucketAggDependencies; @@ -32,10 +33,11 @@ describe('date_range params', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index a42cb70a62b7d..fe013928bba65 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -107,7 +107,7 @@ export const getFiltersBucketAgg = ({ (typeof filter.input.query === 'string' ? filter.input.query : toAngularJSON(filter.input.query)); - filters[label] = { query }; + filters[label] = query; }, {} ); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts index 226faefe43482..877a817984dc6 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -24,6 +24,7 @@ import { BUCKET_TYPES } from './bucket_agg_types'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; +import { InternalStartServices } from '../../../types'; describe('Geohash Agg', () => { let aggTypesDependencies: GeoHashBucketAggDependencies; @@ -31,10 +32,11 @@ describe('Geohash Agg', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; geoHashBucketAgg = getGeoHashBucketAgg(aggTypesDependencies); diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts index a55c32951232a..4756669f5b4b3 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts @@ -29,6 +29,7 @@ import { } from './histogram'; import { BucketAggType } from './bucket_agg_type'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; +import { InternalStartServices } from '../../../types'; describe('Histogram Agg', () => { let aggTypesDependencies: HistogramBucketAggDependencies; @@ -38,10 +39,11 @@ describe('Histogram Agg', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts b/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts index 0beeb1c372275..116f8cfad60f6 100644 --- a/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts +++ b/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts @@ -21,20 +21,22 @@ import { isString, isObject } from 'lodash'; import { IBucketAggConfig, BucketAggType, BucketAggParam } from './bucket_agg_type'; import { IAggConfig } from '../agg_config'; -export const isType = (type: string) => { +export const isType = (...types: string[]) => { return (agg: IAggConfig): boolean => { const field = agg.params.field; - return field && field.type === type; + return types.some(type => field && field.type === type); }; }; +export const isNumberType = isType('number'); export const isStringType = isType('string'); +export const isStringOrNumberType = isType('string', 'number'); export const migrateIncludeExcludeFormat = { serialize(this: BucketAggParam, value: any, agg: IBucketAggConfig) { if (this.shouldShow && !this.shouldShow(agg)) return; - if (!value || isString(value)) return value; + if (!value || isString(value) || Array.isArray(value)) return value; else return value.pattern; }, write( @@ -44,7 +46,12 @@ export const migrateIncludeExcludeFormat = { ) { const value = aggConfig.getParam(this.name); - if (isObject(value)) { + if (Array.isArray(value) && value.length > 0 && isNumberType(aggConfig)) { + const parsedValue = value.filter((val): val is number => Number.isFinite(val)); + if (parsedValue.length) { + output.params[this.name] = parsedValue; + } + } else if (isObject(value)) { output.params[this.name] = value.pattern; } else if (value && isStringType(aggConfig)) { output.params[this.name] = value; diff --git a/src/plugins/data/public/search/aggs/buckets/range.test.ts b/src/plugins/data/public/search/aggs/buckets/range.test.ts index 144d2b779e950..4c2d3af1ab734 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.test.ts @@ -24,6 +24,7 @@ import { BUCKET_TYPES } from './bucket_agg_types'; import { FieldFormatsGetConfigFn, NumberFormat } from '../../../../common'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; const buckets = [ { @@ -50,10 +51,11 @@ describe('Range Agg', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; mockDataServices(); diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts index d0ace5a50c28d..156f7f8108482 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts @@ -26,6 +26,7 @@ import { } from './significant_terms'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('Significant Terms Agg', () => { describe('order agg editor UI', () => { @@ -34,10 +35,11 @@ describe('Significant Terms Agg', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/terms.test.ts b/src/plugins/data/public/search/aggs/buckets/terms.test.ts index 0dc052bd1fdf6..9769efb6da749 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.test.ts @@ -75,5 +75,65 @@ describe('Terms Agg', () => { expect(params.include).toBe('404'); expect(params.exclude).toBe('400'); }); + + test('accepts string from string field type and writes this value', () => { + const aggConfigs = getAggConfigs({ + include: 'include value', + exclude: 'exclude value', + field: { + name: 'string_field', + type: 'string', + }, + orderAgg: { + type: 'count', + }, + }); + + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params.field).toBe('string_field'); + expect(params.include).toBe('include value'); + expect(params.exclude).toBe('exclude value'); + }); + + test('accepts empty array from number field type and does not write a value', () => { + const aggConfigs = getAggConfigs({ + include: [], + exclude: [], + field: { + name: 'empty_number_field', + type: 'number', + }, + orderAgg: { + type: 'count', + }, + }); + + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params.field).toBe('empty_number_field'); + expect(params.include).toBe(undefined); + expect(params.exclude).toBe(undefined); + }); + + test('filters array with empty strings from number field type and writes only numbers', () => { + const aggConfigs = getAggConfigs({ + include: [1.1, 2, '', 3.33, ''], + exclude: ['', 4, 5.555, '', 6], + field: { + name: 'number_field', + type: 'number', + }, + orderAgg: { + type: 'count', + }, + }); + + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params.field).toBe('number_field'); + expect(params.include).toStrictEqual([1.1, 2, 3.33]); + expect(params.exclude).toStrictEqual([4, 5.555, 6]); + }); }); }); diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index 5baa38af0e8d6..a12a1d7ac2d3d 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -22,8 +22,11 @@ import { i18n } from '@kbn/i18n'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterTerms } from './create_filter/terms'; -import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; -import { IAggConfigs } from '../agg_configs'; +import { + isStringOrNumberType, + migrateIncludeExcludeFormat, +} from './migrate_include_exclude_format'; +import { AggConfigSerialized, IAggConfigs } from '../types'; import { Adapters } from '../../../../../inspector/public'; import { ISearchSource } from '../../search_source'; @@ -60,10 +63,27 @@ export interface TermsBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsTerms { + field: string; + order: 'asc' | 'desc'; + orderBy: string; + orderAgg?: AggConfigSerialized; + size?: number; + missingBucket?: boolean; + missingBucketLabel?: string; + otherBucket?: boolean; + otherBucketLabel?: string; + // advanced + exclude?: string; + include?: string; + json?: string; +} + export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) => new BucketAggType( { name: BUCKET_TYPES.TERMS, + expressionName: 'aggTerms', title: termsTitle, makeLabel(agg) { const params = agg.params; @@ -151,8 +171,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe type: 'agg', allowedAggs: termsAggFilter, default: null, - makeAgg(termsAgg, state) { - state = state || {}; + makeAgg(termsAgg, state = { type: 'count' }) { state.schema = 'orderAgg'; const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false, @@ -266,7 +285,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe }), type: 'string', advanced: true, - shouldShow: isStringType, + shouldShow: isStringOrNumberType, ...migrateIncludeExcludeFormat, }, { @@ -276,7 +295,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe }), type: 'string', advanced: true, - shouldShow: isStringType, + shouldShow: isStringOrNumberType, ...migrateIncludeExcludeFormat, }, ], diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts new file mode 100644 index 0000000000000..f55f1de796013 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts @@ -0,0 +1,164 @@ +/* + * 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. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggTerms } from './terms_fn'; + +describe('agg_expression_functions', () => { + describe('aggTerms', () => { + const fn = functionWrapper(aggTerms()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + order: 'desc', + orderBy: '2', + size: 6, + missingBucket: true, + missingBucketLabel: 'missing', + otherBucket: true, + otherBucketLabel: 'other', + exclude: 'ios', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "exclude": "ios", + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": true, + "missingBucketLabel": "missing", + "order": "desc", + "orderAgg": undefined, + "orderBy": "2", + "otherBucket": true, + "otherBucketLabel": "other", + "size": 6, + }, + "schema": "whatever", + "type": "terms", + } + `); + }); + + test('handles orderAgg as a subexpression', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + orderAgg: fn({ field: 'name', order: 'asc', orderBy: '1' }), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "name", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts new file mode 100644 index 0000000000000..7980bfabe79fb --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts @@ -0,0 +1,181 @@ +/* + * 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. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs } from '../'; + +const aggName = 'terms'; +const fnName = 'aggTerms'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +// Since the orderAgg param is an agg nested in a subexpression, we need to +// overwrite the param type to expect a value of type AggExpressionType. +type Arguments = AggArgs & + Assign< + AggArgs, + { orderAgg?: AggArgs['orderAgg'] extends undefined ? undefined : AggExpressionType } + >; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggTerms = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.terms.help', { + defaultMessage: 'Generates a serialized agg config for a terms agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.terms.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + order: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.order.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + orderBy: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.orderBy.help', { + defaultMessage: 'Field to order results by', + }), + }, + orderAgg: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.buckets.terms.orderAgg.help', { + defaultMessage: 'Agg config to use for ordering results', + }), + }, + size: { + types: ['number'], + default: 5, + help: i18n.translate('data.search.aggs.buckets.terms.size.help', { + defaultMessage: 'Max number of buckets to retrieve', + }), + }, + missingBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.missingBucket.help', { + defaultMessage: 'When set to true, groups together any buckets with missing fields', + }), + }, + missingBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', { + defaultMessage: 'Missing', + description: `Default label used in charts when documents are missing a field. + Visible when you create a chart with a terms aggregation and enable "Show missing values"`, + }), + help: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel.help', { + defaultMessage: 'Default label used in charts when documents are missing a field.', + }), + }, + otherBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.otherBucket.help', { + defaultMessage: 'When set to true, groups together any buckets beyond the allowed size', + }), + }, + otherBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', { + defaultMessage: 'Other', + }), + help: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel.help', { + defaultMessage: 'Default label used in charts for documents in the Other bucket', + }), + }, + exclude: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.exclude.help', { + defaultMessage: 'Specific bucket values to exclude from results', + }), + }, + include: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.include.help', { + defaultMessage: 'Specific bucket values to include in results', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + let json; + try { + json = args.json ? JSON.parse(args.json) : undefined; + } catch (e) { + throw new Error('Unable to parse json argument string'); + } + + // Need to spread this object to work around TS bug: + // https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742 + const orderAgg = args.orderAgg?.value ? { ...args.orderAgg.value } : undefined; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: aggName, + params: { + ...rest, + orderAgg, + json, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/index.test.ts b/src/plugins/data/public/search/aggs/index.test.ts index 419c3fdab1caf..382c5a10c2da5 100644 --- a/src/plugins/data/public/search/aggs/index.test.ts +++ b/src/plugins/data/public/search/aggs/index.test.ts @@ -24,6 +24,7 @@ import { isBucketAggType } from './buckets/bucket_agg_type'; import { isMetricAggType } from './metrics/metric_agg_type'; import { QueryStart } from '../../query'; import { FieldFormatsStart } from '../../field_formats'; +import { InternalStartServices } from '../../types'; describe('AggTypesComponent', () => { const coreSetup = coreMock.createSetup(); @@ -32,10 +33,11 @@ describe('AggTypesComponent', () => { const aggTypes = getAggTypes({ uiSettings: coreSetup.uiSettings, query: {} as QueryStart, - getInternalStartServices: () => ({ - notifications: coreStart.notifications, - fieldFormats: {} as FieldFormatsStart, - }), + getInternalStartServices: () => + (({ + notifications: coreStart.notifications, + fieldFormats: {} as FieldFormatsStart, + } as unknown) as InternalStartServices), }); const { buckets, metrics } = aggTypes; diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 3868d8f1bcd16..947394c97bdcd 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -36,14 +36,14 @@ const metricAggFilter = [ '!geo_centroid', ]; -const parentPipelineType = i18n.translate( +export const parentPipelineType = i18n.translate( 'data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle', { defaultMessage: 'Parent Pipeline Aggregations', } ); -const parentPipelineAggHelper = { +export const parentPipelineAggHelper = { subtype: parentPipelineType, params() { return [ @@ -56,13 +56,9 @@ const parentPipelineAggHelper = { name: 'customMetric', type: 'agg', allowedAggs: metricAggFilter, - makeAgg(termsAgg, state: any) { - state = state || { type: 'count' }; - + makeAgg(termsAgg, state = { type: 'count' }) { const metricAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); - metricAgg.id = termsAgg.id + '-metric'; - return metricAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -89,5 +85,3 @@ const parentPipelineAggHelper = { return subAgg ? subAgg.type.getFormat(subAgg) : new (FieldFormat.from(identity))(); }, }; - -export { parentPipelineAggHelper, parentPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index c1d05a39285b7..cee7841a8c3b9 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -43,14 +43,14 @@ const metricAggFilter: string[] = [ ]; const bucketAggFilter: string[] = []; -const siblingPipelineType = i18n.translate( +export const siblingPipelineType = i18n.translate( 'data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle', { defaultMessage: 'Sibling pipeline aggregations', } ); -const siblingPipelineAggHelper = { +export const siblingPipelineAggHelper = { subtype: siblingPipelineType, params() { return [ @@ -59,11 +59,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: bucketAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'date_histogram' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-bucket'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -76,11 +74,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: metricAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'count' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'count' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-metric'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -98,5 +94,3 @@ const siblingPipelineAggHelper = { : new (FieldFormat.from(identity))(); }, }; - -export { siblingPipelineAggHelper, siblingPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/median.test.ts b/src/plugins/data/public/search/aggs/metrics/median.test.ts index de3ca646ead9e..71c48f04a3ca8 100644 --- a/src/plugins/data/public/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/median.test.ts @@ -23,14 +23,16 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypeMetricMedianProvider class', () => { let aggConfigs: IAggConfigs; const aggTypesDependencies: MedianMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; beforeEach(() => { diff --git a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts index 3beb92a2fa000..f386034ea8a7b 100644 --- a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts @@ -25,14 +25,15 @@ import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { GetInternalStartServicesFn } from '../../../types'; +import { GetInternalStartServicesFn, InternalStartServices } from '../../../types'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; describe('parent pipeline aggs', function() { - const getInternalStartServices: GetInternalStartServicesFn = () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }); + const getInternalStartServices: GetInternalStartServicesFn = () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices); const typesRegistry = mockAggTypesRegistry(); diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index 1b94ecd602075..7491f15aa3002 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -25,19 +25,28 @@ import { import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +import { FieldFormatsStart } from '../../../field_formats'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentileRanksProvider class', function() { let aggConfigs: IAggConfigs; - const aggTypesDependencies: PercentileRanksMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), - }; + let fieldFormats: FieldFormatsStart; + let aggTypesDependencies: PercentileRanksMetricAggDependencies; beforeEach(() => { + fieldFormats = fieldFormatsServiceMock.createStartContract(); + fieldFormats.getDefaultInstance = (() => ({ + convert: (t?: string) => t, + })) as any; + aggTypesDependencies = { + getInternalStartServices: () => + (({ + fieldFormats, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), + }; const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]); const field = { name: 'bytes', @@ -59,12 +68,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { type: METRIC_TYPES.PERCENTILE_RANKS, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => x), - }, - }, + field: 'bytes', customLabel: 'my custom field label', values: [5000, 10000], }, diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts index 76da2fe3eb62c..76382c01bcc10 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -27,14 +27,16 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentilesProvider class', () => { let aggConfigs: IAggConfigs; const aggTypesDependencies: PercentilesMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; beforeEach(() => { @@ -59,12 +61,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { type: METRIC_TYPES.PERCENTILES, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => `${x}th`), - }, - }, + field: 'bytes', customLabel: 'prince', percents: [95], }, diff --git a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts index a47aa2c677ade..5e1834d3b4935 100644 --- a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts @@ -26,14 +26,15 @@ import { AggConfigs } from '../agg_configs'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { mockAggTypesRegistry } from '../test_helpers'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { GetInternalStartServicesFn } from '../../../types'; +import { GetInternalStartServicesFn, InternalStartServices } from '../../../types'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; describe('sibling pipeline aggs', () => { - const getInternalStartServices: GetInternalStartServicesFn = () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }); + const getInternalStartServices: GetInternalStartServicesFn = () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices); const typesRegistry = mockAggTypesRegistry(); diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts index d2370e1fed02c..536764b2bcf0b 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts @@ -27,13 +27,15 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypeMetricStandardDeviationProvider class', () => { const aggTypesDependencies: StdDeviationMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; const typesRegistry = mockAggTypesRegistry([getStdDeviationMetricAgg(aggTypesDependencies)]); const getAggConfigs = (customLabel?: string) => { diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts index 142b8e4c83301..617e458cf6243 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts @@ -25,15 +25,17 @@ import { IMetricAggConfig } from './metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../common'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('Top hit metric', () => { let aggDsl: Record; let aggConfig: IMetricAggConfig; const aggTypesDependencies: TopHitMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; const init = ({ diff --git a/src/plugins/data/public/search/aggs/param_types/agg.ts b/src/plugins/data/public/search/aggs/param_types/agg.ts index e5b53020c3159..e3f8c7c922170 100644 --- a/src/plugins/data/public/search/aggs/param_types/agg.ts +++ b/src/plugins/data/public/search/aggs/param_types/agg.ts @@ -17,13 +17,13 @@ * under the License. */ -import { AggConfig, IAggConfig } from '../agg_config'; +import { AggConfig, IAggConfig, AggConfigSerialized } from '../agg_config'; import { BaseParamType } from './base'; export class AggParamType extends BaseParamType< TAggConfig > { - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; allowedAggs: string[] = []; constructor(config: Record) { @@ -42,17 +42,25 @@ export class AggParamType extends Ba } if (!config.serialize) { this.serialize = (agg: TAggConfig) => { - return agg.toJSON(); + return agg.serialize(); }; } if (!config.deserialize) { - this.deserialize = (state: unknown, agg?: TAggConfig): TAggConfig => { + this.deserialize = (state: AggConfigSerialized, agg?: TAggConfig): TAggConfig => { if (!agg) { throw new Error('aggConfig was not provided to AggParamType deserialize function'); } return this.makeAgg(agg, state); }; } + if (!config.toExpressionAst) { + this.toExpressionAst = (agg: TAggConfig) => { + if (!agg || !agg.toExpressionAst) { + throw new Error('aggConfig was not provided to AggParamType toExpressionAst function'); + } + return agg.toExpressionAst(); + }; + } this.makeAgg = config.makeAgg; this.valueType = AggConfig; diff --git a/src/plugins/data/public/search/aggs/param_types/base.ts b/src/plugins/data/public/search/aggs/param_types/base.ts index 2cbc5866e284d..a6f7e5adea043 100644 --- a/src/plugins/data/public/search/aggs/param_types/base.ts +++ b/src/plugins/data/public/search/aggs/param_types/base.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ExpressionAstFunction } from 'src/plugins/expressions/public'; import { IAggConfigs } from '../agg_configs'; import { IAggConfig } from '../agg_config'; import { FetchOptions } from '../../fetch'; @@ -37,6 +38,7 @@ export class BaseParamType { ) => void; serialize: (value: any, aggConfig?: TAggConfig) => any; deserialize: (value: any, aggConfig?: TAggConfig) => any; + toExpressionAst?: (value: any) => ExpressionAstFunction | undefined; options: any[]; valueType?: any; @@ -77,6 +79,7 @@ export class BaseParamType { this.write = config.write || defaultWrite; this.serialize = config.serialize; this.deserialize = config.deserialize; + this.toExpressionAst = config.toExpressionAst; this.options = config.options; this.modifyAggConfigOnSearchRequestStart = config.modifyAggConfigOnSearchRequestStart || function() {}; diff --git a/src/plugins/data/public/search/aggs/param_types/field.test.ts b/src/plugins/data/public/search/aggs/param_types/field.test.ts index ea7931130b84a..2c51d9709f906 100644 --- a/src/plugins/data/public/search/aggs/param_types/field.test.ts +++ b/src/plugins/data/public/search/aggs/param_types/field.test.ts @@ -23,13 +23,15 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../common'; import { IAggConfig } from '../agg_config'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('Field', () => { const fieldParamTypeDependencies: FieldParamTypeDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; const indexPattern = { diff --git a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts new file mode 100644 index 0000000000000..cb0e37c0296d7 --- /dev/null +++ b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts @@ -0,0 +1,49 @@ +/* + * 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. + */ + +import { mapValues } from 'lodash'; +import { + AnyExpressionFunctionDefinition, + ExpressionFunctionDefinition, + ExecutionContext, +} from '../../../../../../plugins/expressions/public'; + +/** + * Takes a function spec and passes in default args, + * overriding with any provided args. + * + * Similar to the test helper used in Expressions & Canvas, + * however in this case we are ignoring the input & execution + * context, as they are not applicable to the agg type + * expression functions. + */ +export const functionWrapper = (spec: T) => { + const defaultArgs = mapValues(spec.args, argSpec => argSpec.default); + return ( + args: T extends ExpressionFunctionDefinition< + infer Name, + infer Input, + infer Arguments, + infer Output, + infer Context + > + ? Arguments + : never + ) => spec.fn(null, { ...defaultArgs, ...args }, {} as ExecutionContext); +}; diff --git a/src/plugins/data/public/search/aggs/test_helpers/index.ts b/src/plugins/data/public/search/aggs/test_helpers/index.ts index 131f921586144..63f8ae0ce5f58 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/index.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/index.ts @@ -17,5 +17,6 @@ * under the License. */ +export { functionWrapper } from './function_wrapper'; export { mockAggTypesRegistry } from './mock_agg_types_registry'; export { mockDataServices } from './mock_data_services'; diff --git a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts index 2383affa2a8c5..3ff2fbf35ad7e 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts @@ -17,7 +17,6 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks'; import { AggTypesRegistry, AggTypesRegistryStart } from '../agg_types_registry'; import { getAggTypes } from '../agg_types'; @@ -25,6 +24,7 @@ import { BucketAggType } from '../buckets/bucket_agg_type'; import { MetricAggType } from '../metrics/metric_agg_type'; import { queryServiceMock } from '../../../query/mocks'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; +import { InternalStartServices } from '../../../types'; /** * Testing utility which creates a new instance of AggTypesRegistry, @@ -53,14 +53,19 @@ export function mockAggTypesRegistry | MetricAggTyp } }); } else { - const core = coreMock.createSetup(); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const aggTypes = getAggTypes({ - uiSettings: core.uiSettings, + uiSettings: coreSetup.uiSettings, query: queryServiceMock.createSetupContract(), - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + } as unknown) as InternalStartServices), }); aggTypes.buckets.forEach(type => registrySetup.registerBucket(type)); diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index 4b2b1620ad1d3..95a7a45013567 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -19,21 +19,23 @@ import { IndexPattern } from '../../index_patterns'; import { + AggConfig, + AggConfigSerialized, + AggConfigs, + AggParamsTerms, AggType, + aggTypeFieldFilters, AggTypesRegistrySetup, AggTypesRegistryStart, - AggConfig, - AggConfigs, CreateAggConfigParams, FieldParamType, getCalculateAutoTimeExpression, MetricAggType, - aggTypeFieldFilters, parentPipelineAggHelper, siblingPipelineAggHelper, } from './'; -export { IAggConfig } from './agg_config'; +export { IAggConfig, AggConfigSerialized } from './agg_config'; export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; export { IAggType } from './agg_type'; export { AggParam, AggParamOption } from './agg_params'; @@ -70,3 +72,25 @@ export interface SearchAggsStart { ) => InstanceType; types: AggTypesRegistryStart; } + +/** @internal */ +export interface AggExpressionType { + type: 'agg_type'; + value: AggConfigSerialized; +} + +/** @internal */ +export type AggExpressionFunctionArgs< + Name extends keyof AggParamsMapping +> = AggParamsMapping[Name] & Pick; + +/** + * A global list of the param interfaces for each agg type. + * For now this is internal, but eventually we will probably + * want to make it public. + * + * @internal + */ +export interface AggParamsMapping { + terms: AggParamsTerms; +} diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 2341f4fe447db..087b83127079f 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -30,7 +30,7 @@ import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; -import { ISearchSource, SearchSource } from '../search_source'; +import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common'; import { FilterManager, getTime } from '../../query'; @@ -253,7 +253,8 @@ export const esaggs = (): ExpressionFunctionDefinition { test('Passes the additional arguments it is given to the search strategy', () => { const searchRequests = [{ _searchStrategyId: 0 }]; - const args = { searchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; + const args = { legacySearchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; callClient(searchRequests, [], args); diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts index 835b02b3cd5c7..9e3d65a69bf02 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts @@ -62,10 +62,10 @@ describe('defaultSearchStrategy', function() { }, ], esShardTimeout: 0, - searchService, + legacySearchService: searchService.__LEGACY, }; - es = searchArgs.searchService.__LEGACY.esClient; + es = searchArgs.legacySearchService.esClient; }); test('does not send max_concurrent_shard_requests by default', async () => { diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.ts b/src/plugins/data/public/search/legacy/default_search_strategy.ts index 1552410f9090c..3216803dcbfa2 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.ts @@ -32,11 +32,11 @@ export const defaultSearchStrategy: SearchStrategyProvider = { function msearch({ searchRequests, - searchService, + legacySearchService, config, esShardTimeout, }: SearchStrategySearchParams) { - const es = searchService.__LEGACY.esClient; + const es = legacySearchService.esClient; const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { const inlineHeader = { index: index.title || index, diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index cb1c625a72959..dd196074c8173 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -20,20 +20,19 @@ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { AggTypeFieldFilters } from './aggs/param_types/filter'; import { ISearchStart } from './types'; +import { searchSourceMock, createSearchSourceMock } from './search_source/mocks'; -export * from './search_source/mocks'; - -export const searchSetupMock = { +const searchSetupMock = { aggs: searchAggsSetupMock(), registerSearchStrategyContext: jest.fn(), registerSearchStrategyProvider: jest.fn(), }; -export const searchStartMock: jest.Mocked = { +const searchStartMock: jest.Mocked = { aggs: searchAggsStartMock(), setInterceptor: jest.fn(), search: jest.fn(), - createSearchSource: jest.fn(), + searchSource: searchSourceMock, __LEGACY: { AggConfig: jest.fn() as any, AggType: jest.fn(), @@ -48,3 +47,5 @@ export const searchStartMock: jest.Mocked = { }, }, }; + +export { searchSetupMock, searchStartMock, createSearchSourceMock }; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 19308dd387d3a..b1f7925bec4bb 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -18,9 +18,10 @@ */ import { coreMock } from '../../../../core/public/mocks'; +import { CoreSetup } from '../../../../core/public'; +import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks'; import { SearchService } from './search_service'; -import { CoreSetup } from '../../../../core/public'; describe('Search service', () => { let searchService: SearchService; @@ -35,6 +36,7 @@ describe('Search service', () => { it('exposes proper contract', async () => { const setup = searchService.setup(mockCoreSetup, { packageInfo: { version: '8' }, + expressions: expressionsPluginMock.createSetupContract(), } as any); expect(setup).toHaveProperty('registerSearchStrategyProvider'); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 916278a96659b..b59524baa9fa7 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -18,20 +18,27 @@ */ import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public'; +import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy'; +import { + createSearchSourceFromJSON, + SearchSource, + SearchSourceDependencies, + SearchSourceFields, +} from './search_source'; import { ISearchSetup, ISearchStart, TSearchStrategyProvider, TSearchStrategiesMap } from './types'; import { TStrategyTypes } from './strategy_types'; import { getEsClient, LegacyApiCaller } from './legacy'; import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search'; import { esSearchStrategyProvider } from './es_search'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; -import { createSearchSource } from './search_source'; import { QuerySetup } from '../query'; import { GetInternalStartServicesFn } from '../types'; import { SearchInterceptor } from './search_interceptor'; import { getAggTypes, + getAggTypesFunctions, AggType, AggTypesRegistry, AggConfig, @@ -43,18 +50,19 @@ import { parentPipelineAggHelper, siblingPipelineAggHelper, } from './aggs'; - import { FieldFormatsStart } from '../field_formats'; +import { ISearchGeneric } from './i_search'; interface SearchServiceSetupDependencies { + expressions: ExpressionsSetup; + getInternalStartServices: GetInternalStartServicesFn; packageInfo: PackageInfo; query: QuerySetup; - getInternalStartServices: GetInternalStartServicesFn; } -interface SearchStartDependencies { - fieldFormats: FieldFormatsStart; +interface SearchServiceStartDependencies { indexPatterns: IndexPatternsContract; + fieldFormats: FieldFormatsStart; } /** @@ -92,22 +100,27 @@ export class SearchService implements Plugin { public setup( core: CoreSetup, - { packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies + { expressions, packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies ): ISearchSetup { this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo); this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider); this.registerSearchStrategyProvider(ES_SEARCH_STRATEGY, esSearchStrategyProvider); const aggTypesSetup = this.aggTypesRegistry.setup(); + + // register each agg type const aggTypes = getAggTypes({ query, uiSettings: core.uiSettings, getInternalStartServices, }); - aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b)); aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m)); + // register expression functions for each agg type + const aggFunctions = getAggTypesFunctions(); + aggFunctions.forEach(fn => expressions.registerFunction(fn)); + return { aggs: { calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), @@ -117,10 +130,7 @@ export class SearchService implements Plugin { }; } - public start( - core: CoreStart, - { fieldFormats, indexPatterns }: SearchStartDependencies - ): ISearchStart { + public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -135,40 +145,54 @@ export class SearchService implements Plugin { const aggTypesStart = this.aggTypesRegistry.start(); + const search: ISearchGeneric = (request, options, strategyName) => { + const strategyProvider = this.getSearchStrategy(strategyName || DEFAULT_SEARCH_STRATEGY); + const searchStrategy = strategyProvider({ + core, + getSearchStrategy: this.getSearchStrategy, + }); + return this.searchInterceptor.search(searchStrategy.search as any, request, options); + }; + + const legacySearch = { + esClient: this.esClient!, + AggConfig, + AggType, + aggTypeFieldFilters, + FieldParamType, + MetricAggType, + parentPipelineAggHelper, + siblingPipelineAggHelper, + }; + + const searchSourceDependencies: SearchSourceDependencies = { + uiSettings: core.uiSettings, + injectedMetadata: core.injectedMetadata, + search, + legacySearch, + }; + return { aggs: { calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { + fieldFormats: dependencies.fieldFormats, typesRegistry: aggTypesStart, - fieldFormats, }); }, types: aggTypesStart, }, - search: (request, options, strategyName) => { - const strategyProvider = this.getSearchStrategy(strategyName || DEFAULT_SEARCH_STRATEGY); - const { search } = strategyProvider({ - core, - getSearchStrategy: this.getSearchStrategy, - }); - return this.searchInterceptor.search(search as any, request, options); + search, + searchSource: { + create: (fields?: SearchSourceFields) => new SearchSource(fields, searchSourceDependencies), + fromJSON: createSearchSourceFromJSON(dependencies.indexPatterns, searchSourceDependencies), }, setInterceptor: (searchInterceptor: SearchInterceptor) => { // TODO: should an intercepror have a destroy method? this.searchInterceptor = searchInterceptor; }, - createSearchSource: createSearchSource(indexPatterns), - __LEGACY: { - esClient: this.esClient!, - AggConfig, - AggType, - aggTypeFieldFilters, - FieldParamType, - MetricAggType, - parentPipelineAggHelper, - siblingPipelineAggHelper, - }, + __LEGACY: legacySearch, }; } diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts index d49ce5a0d11f8..efa63b0722e28 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -16,30 +16,43 @@ * specific language governing permissions and limitations * under the License. */ -import { createSearchSource as createSearchSourceFactory } from './create_search_source'; +import { createSearchSourceFromJSON } from './create_search_source'; import { IIndexPattern } from '../../../common/index_patterns'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { Filter } from '../../../common/es_query/filters'; +import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../mocks'; -describe('createSearchSource', function() { - let createSearchSource: ReturnType; +describe('createSearchSource', () => { const indexPatternMock: IIndexPattern = {} as IIndexPattern; let indexPatternContractMock: jest.Mocked; + let dependencies: any; + let createSearchSource: ReturnType; beforeEach(() => { + const core = coreMock.createStart(); + const data = dataPluginMock.createStartContract(); + + dependencies = { + searchService: data.search, + uiSettings: core.uiSettings, + injectedMetadata: core.injectedMetadata, + }; + indexPatternContractMock = ({ get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), } as unknown) as jest.Mocked; - createSearchSource = createSearchSourceFactory(indexPatternContractMock); + + createSearchSource = createSearchSourceFromJSON(indexPatternContractMock, dependencies); }); - it('should fail if JSON is invalid', () => { + test('should fail if JSON is invalid', () => { expect(createSearchSource('{', [])).rejects.toThrow(); expect(createSearchSource('0', [])).rejects.toThrow(); expect(createSearchSource('"abcdefg"', [])).rejects.toThrow(); }); - it('should set fields', async () => { + test('should set fields', async () => { const searchSource = await createSearchSource( JSON.stringify({ highlightAll: true, @@ -50,6 +63,7 @@ describe('createSearchSource', function() { }), [] ); + expect(searchSource.getOwnField('highlightAll')).toBe(true); expect(searchSource.getOwnField('query')).toEqual({ query: '', @@ -57,7 +71,7 @@ describe('createSearchSource', function() { }); }); - it('should resolve referenced index pattern', async () => { + test('should resolve referenced index pattern', async () => { const searchSource = await createSearchSource( JSON.stringify({ indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', @@ -70,11 +84,12 @@ describe('createSearchSource', function() { }, ] ); + expect(indexPatternContractMock.get).toHaveBeenCalledWith('123-456'); expect(searchSource.getOwnField('index')).toBe(indexPatternMock); }); - it('should set filters and resolve referenced index patterns', async () => { + test('should set filters and resolve referenced index patterns', async () => { const searchSource = await createSearchSource( JSON.stringify({ filter: [ @@ -110,6 +125,7 @@ describe('createSearchSource', function() { ] ); const filters = searchSource.getOwnField('filter') as Filter[]; + expect(filters[0]).toMatchInlineSnapshot(` Object { "$state": Object { @@ -135,7 +151,7 @@ describe('createSearchSource', function() { `); }); - it('should migrate legacy queries on the fly', async () => { + test('should migrate legacy queries on the fly', async () => { const searchSource = await createSearchSource( JSON.stringify({ highlightAll: true, @@ -143,6 +159,7 @@ describe('createSearchSource', function() { }), [] ); + expect(searchSource.getOwnField('query')).toEqual({ query: 'a:b', language: 'lucene', diff --git a/src/plugins/data/public/search/search_source/create_search_source.ts b/src/plugins/data/public/search/search_source/create_search_source.ts index 35b7ac4eb9762..cc98f433b3a03 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.ts @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import _ from 'lodash'; +import { transform, defaults, isFunction } from 'lodash'; import { SavedObjectReference } from 'kibana/public'; import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; import { InvalidJSONProperty } from '../../../../kibana_utils/public'; -import { SearchSource } from './search_source'; +import { SearchSourceDependencies, SearchSource, ISearchSource } from './search_source'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { SearchSourceFields } from './types'; @@ -38,12 +38,16 @@ import { SearchSourceFields } from './types'; * returned by `serializeSearchSource` and `references`, a list of references including the ones * returned by `serializeSearchSource`. * + * * @public */ -export const createSearchSource = (indexPatterns: IndexPatternsContract) => async ( +export const createSearchSourceFromJSON = ( + indexPatterns: IndexPatternsContract, + searchSourceDependencies: SearchSourceDependencies +) => async ( searchSourceJson: string, references: SavedObjectReference[] -) => { - const searchSource = new SearchSource(); +): Promise => { + const searchSource = new SearchSource({}, searchSourceDependencies); // if we have a searchSource, set its values based on the searchSourceJson field let searchSourceValues: Record; @@ -90,17 +94,17 @@ export const createSearchSource = (indexPatterns: IndexPatternsContract) => asyn } const searchSourceFields = searchSource.getFields(); - const fnProps = _.transform( + const fnProps = transform( searchSourceFields, function(dynamic, val, name) { - if (_.isFunction(val) && name) dynamic[name] = val; + if (isFunction(val) && name) dynamic[name] = val; }, {} ); // This assignment might hide problems because the type of values passed from the parsed JSON // might not fit the SearchSourceFields interface. - const newFields: SearchSourceFields = _.defaults(searchSourceValues, fnProps); + const newFields: SearchSourceFields = defaults(searchSourceValues, fnProps); searchSource.setFields(newFields); const query = searchSource.getOwnField('query'); diff --git a/src/plugins/data/public/search/search_source/index.ts b/src/plugins/data/public/search/search_source/index.ts index 0e9f530d0968a..9c4106b2dc616 100644 --- a/src/plugins/data/public/search/search_source/index.ts +++ b/src/plugins/data/public/search/search_source/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export * from './search_source'; -export { createSearchSource } from './create_search_source'; +export { SearchSource, ISearchSource, SearchSourceDependencies } from './search_source'; +export { createSearchSourceFromJSON } from './create_search_source'; export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index 1ef7c1187a9e0..157331ea87bb0 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -17,9 +17,15 @@ * under the License. */ -import { ISearchSource } from './search_source'; +import { + injectedMetadataServiceMock, + uiSettingsServiceMock, +} from '../../../../../core/public/mocks'; -export const searchSourceMock: MockedKeys = { +import { ISearchSource, SearchSource } from './search_source'; +import { SearchSourceFields } from './types'; + +export const searchSourceInstanceMock: MockedKeys = { setPreferredSearchStrategyId: jest.fn(), setFields: jest.fn().mockReturnThis(), setField: jest.fn().mockReturnThis(), @@ -39,3 +45,21 @@ export const searchSourceMock: MockedKeys = { history: [], serialize: jest.fn(), }; + +export const searchSourceMock = { + create: jest.fn().mockReturnValue(searchSourceInstanceMock), + fromJSON: jest.fn().mockReturnValue(searchSourceInstanceMock), +}; + +export const createSearchSourceMock = (fields?: SearchSourceFields) => + new SearchSource(fields, { + search: jest.fn(), + legacySearch: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, + uiSettings: uiSettingsServiceMock.createStartContract(), + injectedMetadata: injectedMetadataServiceMock.createStartContract(), + }); diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index 6e878844664ad..7783e65889a12 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -16,28 +16,13 @@ * specific language governing permissions and limitations * under the License. */ - +import { Observable } from 'rxjs'; import { SearchSource } from './search_source'; import { IndexPattern, SortDirection } from '../..'; -import { mockDataServices } from '../aggs/test_helpers'; -import { setSearchService } from '../../services'; -import { searchStartMock } from '../mocks'; import { fetchSoon } from '../legacy'; -import { CoreStart } from 'kibana/public'; -import { Observable } from 'rxjs'; - -// Setup search service mock -searchStartMock.search = jest.fn(() => { - return new Observable(subscriber => { - setTimeout(() => { - subscriber.next({ - rawResponse: '', - }); - subscriber.complete(); - }, 100); - }); -}) as any; -setSearchService(searchStartMock); +import { IUiSettingsClient } from '../../../../../core/public'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; jest.mock('../legacy', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), @@ -48,48 +33,70 @@ const getComputedFields = () => ({ scriptFields: [], docvalueFields: [], }); + const mockSource = { excludes: ['foo-*'] }; const mockSource2 = { excludes: ['bar-*'] }; + const indexPattern = ({ title: 'foo', getComputedFields, getSourceFiltering: () => mockSource, } as unknown) as IndexPattern; + const indexPattern2 = ({ title: 'foo', getComputedFields, getSourceFiltering: () => mockSource2, } as unknown) as IndexPattern; -describe('SearchSource', function() { - let uiSettingsMock: jest.Mocked; +describe('SearchSource', () => { + let mockSearchMethod: any; + let searchSourceDependencies: any; + beforeEach(() => { - const { core } = mockDataServices(); - uiSettingsMock = core.uiSettings; - jest.clearAllMocks(); + const core = coreMock.createStart(); + const data = dataPluginMock.createStartContract(); + + mockSearchMethod = jest.fn(() => { + return new Observable(subscriber => { + setTimeout(() => { + subscriber.next({ + rawResponse: '', + }); + subscriber.complete(); + }, 100); + }); + }); + + searchSourceDependencies = { + search: mockSearchMethod, + legacySearch: data.search.__LEGACY, + injectedMetadata: core.injectedMetadata, + uiSettings: core.uiSettings, + }; }); - describe('#setField()', function() { - it('sets the value for the property', function() { - const searchSource = new SearchSource(); + describe('#setField()', () => { + test('sets the value for the property', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('aggs', 5); expect(searchSource.getField('aggs')).toBe(5); }); }); - describe('#getField()', function() { - it('gets the value for the property', function() { - const searchSource = new SearchSource(); + describe('#getField()', () => { + test('gets the value for the property', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('aggs', 5); expect(searchSource.getField('aggs')).toBe(5); }); }); - describe(`#setField('index')`, function() { - describe('auto-sourceFiltering', function() { - describe('new index pattern assigned', function() { - it('generates a searchSource filter', async function() { - const searchSource = new SearchSource(); + describe(`#setField('index')`, () => { + describe('auto-sourceFiltering', () => { + describe('new index pattern assigned', () => { + test('generates a searchSource filter', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); expect(searchSource.getField('index')).toBe(undefined); expect(searchSource.getField('source')).toBe(undefined); searchSource.setField('index', indexPattern); @@ -98,8 +105,8 @@ describe('SearchSource', function() { expect(request._source).toBe(mockSource); }); - it('removes created searchSource filter on removal', async function() { - const searchSource = new SearchSource(); + test('removes created searchSource filter on removal', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern); searchSource.setField('index', undefined); const request = await searchSource.getSearchRequestBody(); @@ -107,9 +114,9 @@ describe('SearchSource', function() { }); }); - describe('new index pattern assigned over another', function() { - it('replaces searchSource filter with new', async function() { - const searchSource = new SearchSource(); + describe('new index pattern assigned over another', () => { + test('replaces searchSource filter with new', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern); searchSource.setField('index', indexPattern2); expect(searchSource.getField('index')).toBe(indexPattern2); @@ -117,8 +124,8 @@ describe('SearchSource', function() { expect(request._source).toBe(mockSource2); }); - it('removes created searchSource filter on removal', async function() { - const searchSource = new SearchSource(); + test('removes created searchSource filter on removal', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern); searchSource.setField('index', indexPattern2); searchSource.setField('index', undefined); @@ -130,8 +137,8 @@ describe('SearchSource', function() { }); describe('#onRequestStart()', () => { - it('should be called when starting a request', async () => { - const searchSource = new SearchSource({ index: indexPattern }); + test('should be called when starting a request', async () => { + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const fn = jest.fn(); searchSource.onRequestStart(fn); const options = {}; @@ -139,9 +146,9 @@ describe('SearchSource', function() { expect(fn).toBeCalledWith(searchSource, options); }); - it('should not be called on parent searchSource', async () => { - const parent = new SearchSource(); - const searchSource = new SearchSource({ index: indexPattern }); + test('should not be called on parent searchSource', async () => { + const parent = new SearchSource({}, searchSourceDependencies); + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const fn = jest.fn(); searchSource.onRequestStart(fn); @@ -154,9 +161,12 @@ describe('SearchSource', function() { expect(parentFn).not.toBeCalled(); }); - it('should be called on parent searchSource if callParentStartHandlers is true', async () => { - const parent = new SearchSource(); - const searchSource = new SearchSource({ index: indexPattern }).setParent(parent, { + test('should be called on parent searchSource if callParentStartHandlers is true', async () => { + const parent = new SearchSource({}, searchSourceDependencies); + const searchSource = new SearchSource( + { index: indexPattern }, + searchSourceDependencies + ).setParent(parent, { callParentStartHandlers: true, }); @@ -174,19 +184,21 @@ describe('SearchSource', function() { describe('#legacy fetch()', () => { beforeEach(() => { - uiSettingsMock.get.mockImplementation(() => { - return true; // batchSearches = true - }); - }); + const core = coreMock.createStart(); - afterEach(() => { - uiSettingsMock.get.mockImplementation(() => { - return false; // batchSearches = false - }); + searchSourceDependencies = { + ...searchSourceDependencies, + uiSettings: { + ...core.uiSettings, + get: jest.fn(() => { + return true; // batchSearches = true + }), + } as IUiSettingsClient, + }; }); - it('should call msearch', async () => { - const searchSource = new SearchSource({ index: indexPattern }); + test('should call msearch', async () => { + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const options = {}; await searchSource.fetch(options); expect(fetchSoon).toBeCalledTimes(1); @@ -194,18 +206,19 @@ describe('SearchSource', function() { }); describe('#search service fetch()', () => { - it('should call msearch', async () => { - const searchSource = new SearchSource({ index: indexPattern }); + test('should call msearch', async () => { + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const options = {}; + await searchSource.fetch(options); - expect(searchStartMock.search).toBeCalledTimes(1); + expect(mockSearchMethod).toBeCalledTimes(1); }); }); - describe('#serialize', function() { - it('should reference index patterns', () => { + describe('#serialize', () => { + test('should reference index patterns', () => { const indexPattern123 = { id: '123' } as IndexPattern; - const searchSource = new SearchSource(); + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern123); const { searchSourceJSON, references } = searchSource.serialize(); expect(references[0].id).toEqual('123'); @@ -213,8 +226,8 @@ describe('SearchSource', function() { expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); }); - it('should add other fields', () => { - const searchSource = new SearchSource(); + test('should add other fields', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('highlightAll', true); searchSource.setField('from', 123456); const { searchSourceJSON } = searchSource.serialize(); @@ -222,8 +235,8 @@ describe('SearchSource', function() { expect(JSON.parse(searchSourceJSON).from).toEqual(123456); }); - it('should omit sort and size', () => { - const searchSource = new SearchSource(); + test('should omit sort and size', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('highlightAll', true); searchSource.setField('from', 123456); searchSource.setField('sort', { field: SortDirection.asc }); @@ -232,8 +245,8 @@ describe('SearchSource', function() { expect(Object.keys(JSON.parse(searchSourceJSON))).toEqual(['highlightAll', 'from']); }); - it('should serialize filters', () => { - const searchSource = new SearchSource(); + test('should serialize filters', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); const filter = [ { query: 'query', @@ -249,8 +262,8 @@ describe('SearchSource', function() { expect(JSON.parse(searchSourceJSON).filter).toEqual(filter); }); - it('should reference index patterns in filters separately from index field', () => { - const searchSource = new SearchSource(); + test('should reference index patterns in filters separately from index field', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); const indexPattern123 = { id: '123' } as IndexPattern; searchSource.setField('index', indexPattern123); const filter = [ diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 9d2bb889953cf..091a27a6f418d 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -69,34 +69,45 @@ * `appSearchSource`. */ -import _ from 'lodash'; +import { uniqueId, uniq, extend, pick, difference, set, omit, keys, isFunction } from 'lodash'; import { map } from 'rxjs/operators'; -import { SavedObjectReference } from 'kibana/public'; +import { CoreStart, SavedObjectReference } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; -import { IIndexPattern, SearchRequest } from '../..'; +import { IIndexPattern, ISearchGeneric, SearchRequest } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { FetchOptions, RequestFailure, getSearchParams, handleResponse } from '../fetch'; -import { getSearchService, getUiSettings, getInjectedMetadata } from '../../services'; import { getEsQueryConfig, buildEsQuery, Filter } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; import { fetchSoon } from '../legacy'; +import { ISearchStartLegacy } from '../types'; -export type ISearchSource = Pick; +export interface SearchSourceDependencies { + uiSettings: CoreStart['uiSettings']; + search: ISearchGeneric; + legacySearch: ISearchStartLegacy; + injectedMetadata: CoreStart['injectedMetadata']; +} +/** @public **/ export class SearchSource { - private id: string = _.uniqueId('data_source'); + private id: string = uniqueId('data_source'); private searchStrategyId?: string; private parent?: SearchSource; private requestStartHandlers: Array< - (searchSource: ISearchSource, options?: FetchOptions) => Promise + (searchSource: SearchSource, options?: FetchOptions) => Promise > = []; private inheritOptions: SearchSourceOptions = {}; public history: SearchRequest[] = []; + private fields: SearchSourceFields; + private readonly dependencies: SearchSourceDependencies; - constructor(private fields: SearchSourceFields = {}) {} + constructor(fields: SearchSourceFields = {}, dependencies: SearchSourceDependencies) { + this.fields = fields; + this.dependencies = dependencies; + } /** *** * PUBLIC API @@ -147,11 +158,11 @@ export class SearchSource { } create() { - return new SearchSource(); + return new SearchSource({}, this.dependencies); } createCopy() { - const newSearchSource = new SearchSource(); + const newSearchSource = new SearchSource({}, this.dependencies); newSearchSource.setFields({ ...this.fields }); // when serializing the internal fields we lose the internal classes used in the index // pattern, so we have to set it again to workaround this behavior @@ -161,7 +172,7 @@ export class SearchSource { } createChild(options = {}) { - const childSearchSource = new SearchSource(); + const childSearchSource = new SearchSource({}, this.dependencies); childSearchSource.setParent(this, options); return childSearchSource; } @@ -191,16 +202,17 @@ export class SearchSource { * @return {Observable>} */ private fetch$(searchRequest: SearchRequest, signal?: AbortSignal) { - const esShardTimeout = getInjectedMetadata().getInjectedVar('esShardTimeout') as number; - const searchParams = getSearchParams(getUiSettings(), esShardTimeout); + const { search, injectedMetadata, uiSettings } = this.dependencies; + const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number; + const searchParams = getSearchParams(uiSettings, esShardTimeout); const params = { index: searchRequest.index.title || searchRequest.index, body: searchRequest.body, ...searchParams, }; - return getSearchService() - .search({ params, indexType: searchRequest.indexType }, { signal }) - .pipe(map(({ rawResponse }) => handleResponse(searchRequest, rawResponse))); + return search({ params, indexType: searchRequest.indexType }, { signal }).pipe( + map(({ rawResponse }) => handleResponse(searchRequest, rawResponse)) + ); } /** @@ -208,7 +220,9 @@ export class SearchSource { * @return {Promise>} */ private async legacyFetch(searchRequest: SearchRequest, options: FetchOptions) { - const esShardTimeout = getInjectedMetadata().getInjectedVar('esShardTimeout') as number; + const { injectedMetadata, legacySearch, uiSettings } = this.dependencies; + const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number; + return await fetchSoon( searchRequest, { @@ -216,8 +230,8 @@ export class SearchSource { ...options, }, { - searchService: getSearchService(), - config: getUiSettings(), + legacySearchService: legacySearch, + config: uiSettings, esShardTimeout, } ); @@ -228,13 +242,14 @@ export class SearchSource { * @async */ async fetch(options: FetchOptions = {}) { + const { uiSettings } = this.dependencies; await this.requestIsStarting(options); const searchRequest = await this.flatten(); this.history = [searchRequest]; let response; - if (getUiSettings().get('courier:batchSearches')) { + if (uiSettings.get('courier:batchSearches')) { response = await this.legacyFetch(searchRequest, options); } else { response = this.fetch$(searchRequest, options.abortSignal).toPromise(); @@ -253,7 +268,7 @@ export class SearchSource { * @return {undefined} */ onRequestStart( - handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise + handler: (searchSource: SearchSource, options?: FetchOptions) => Promise ) { this.requestStartHandlers.push(handler); } @@ -326,13 +341,15 @@ export class SearchSource { } }; + const { uiSettings } = this.dependencies; + switch (key) { case 'filter': return addToRoot('filters', (data.filters || []).concat(val)); case 'query': return addToRoot(key, (data[key] || []).concat(val)); case 'fields': - const fields = _.uniq((data[key] || []).concat(val)); + const fields = uniq((data[key] || []).concat(val)); return addToRoot(key, fields); case 'index': case 'type': @@ -346,7 +363,7 @@ export class SearchSource { const sort = normalizeSortRequest( val, this.getField('index'), - getUiSettings().get('sort:options') + uiSettings.get('sort:options') ); return addToBody(key, sort); default: @@ -389,7 +406,7 @@ export class SearchSource { body.stored_fields = computedFields.storedFields; body.script_fields = body.script_fields || {}; - _.extend(body.script_fields, computedFields.scriptFields); + extend(body.script_fields, computedFields.scriptFields); const defaultDocValueFields = computedFields.docvalueFields ? computedFields.docvalueFields @@ -400,9 +417,11 @@ export class SearchSource { body._source = index.getSourceFiltering(); } + const { uiSettings } = this.dependencies; + if (body._source) { // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(body._source.excludes, getUiSettings().get('metaFields')); + const filter = fieldWildcardFilter(body._source.excludes, uiSettings.get('metaFields')); body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => filter(docvalueField.field) ); @@ -412,42 +431,22 @@ export class SearchSource { if (fields) { // filter out the docvalue_fields, and script_fields to only include those that we are concerned with body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); - body.script_fields = _.pick(body.script_fields, fields); + body.script_fields = pick(body.script_fields, fields); // request the remaining fields from both stored_fields and _source - const remainingFields = _.difference(fields, _.keys(body.script_fields)); + const remainingFields = difference(fields, keys(body.script_fields)); body.stored_fields = remainingFields; - _.set(body, '_source.includes', remainingFields); + set(body, '_source.includes', remainingFields); } - const esQueryConfigs = getEsQueryConfig(getUiSettings()); + const esQueryConfigs = getEsQueryConfig(uiSettings); body.query = buildEsQuery(index, query, filters, esQueryConfigs); if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, getUiSettings().get('doc_table:highlight')); + body.highlight = getHighlightRequest(body.query, uiSettings.get('doc_table:highlight')); delete searchRequest.highlightAll; } - const translateToQuery = (filter: Filter) => filter && (filter.query || filter); - - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function(id) { - const agg = aggBranch[id]; - - if (agg.filters) { - // translate filters aggregations - const { filters: aggFilters } = agg.filters; - Object.keys(aggFilters).forEach(filterId => { - aggFilters[filterId] = translateToQuery(aggFilters[filterId]); - }); - } - - recurse(agg.aggs || agg.aggregations); - }); - })(body.aggs || body.aggregations); - return searchRequest; } @@ -467,7 +466,7 @@ export class SearchSource { const { filter: originalFilters, ...searchSourceFields - }: Omit = _.omit(this.getFields(), ['sort', 'size']); + }: Omit = omit(this.getFields(), ['sort', 'size']); let serializedSearchSourceFields: Omit & { indexRefName?: string; filter?: Array & { meta: Filter['meta'] & { indexRefName?: string } }>; @@ -524,10 +523,13 @@ export class SearchSource { return filterField; } - if (_.isFunction(filterField)) { + if (isFunction(filterField)) { return this.getFilters(filterField()); } return [filterField]; } } + +/** @public **/ +export type ISearchSource = Pick; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 2122e4e82ec1d..99d111ce1574e 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -17,13 +17,13 @@ * under the License. */ -import { CoreStart } from 'kibana/public'; -import { createSearchSource } from './search_source'; +import { CoreStart, SavedObjectReference } from 'kibana/public'; import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; import { ISearch, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { LegacyApiCaller } from './legacy/es_client'; import { SearchInterceptor } from './search_interceptor'; +import { ISearchSource, SearchSourceFields } from './search_source'; export interface ISearchContext { core: CoreStart; @@ -60,7 +60,7 @@ export type TRegisterSearchStrategyProvider = ( searchStrategyProvider: TSearchStrategyProvider ) => void; -interface ISearchStartLegacy { +export interface ISearchStartLegacy { esClient: LegacyApiCaller; } @@ -81,6 +81,12 @@ export interface ISearchStart { aggs: SearchAggsStart; setInterceptor: (searchInterceptor: SearchInterceptor) => void; search: ISearchGeneric; - createSearchSource: ReturnType; + searchSource: { + create: (fields?: SearchSourceFields) => ISearchSource; + fromJSON: ( + searchSourceJson: string, + references: SavedObjectReference[] + ) => Promise; + }; __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; } diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 199ba17b3b81b..ba0b2de393bde 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { NotificationsStart, CoreSetup, CoreStart } from 'src/core/public'; +import { NotificationsStart, CoreStart } from 'src/core/public'; import { FieldFormatsStart } from './field_formats'; import { createGetterSetter } from '../../kibana_utils/public'; import { IndexPatternsContract } from './index_patterns'; @@ -48,7 +48,7 @@ export const [getQueryService, setQueryService] = createGetterSetter< >('Query'); export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< - CoreSetup['injectedMetadata'] + CoreStart['injectedMetadata'] >('InjectedMetadata'); export const [getSearchService, setSearchService] = createGetterSetter< diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index e24e01d241278..aaef403979de6 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -24,7 +24,7 @@ import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; -import { createFiltersFromEvent } from './actions'; +import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; import { ISearchSetup, ISearchStart } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternSelectProps } from './ui/index_pattern_select'; @@ -49,7 +49,8 @@ export interface DataPublicPluginSetup { export interface DataPublicPluginStart { actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; autocomplete: AutocompleteStart; indexPatterns: IndexPatternsContract; @@ -74,8 +75,11 @@ export interface IDataPluginServices extends Partial { /** @internal **/ export interface InternalStartServices { - fieldFormats: FieldFormatsStart; - notifications: CoreStart['notifications']; + readonly fieldFormats: FieldFormatsStart; + readonly notifications: CoreStart['notifications']; + readonly uiSettings: CoreStart['uiSettings']; + readonly searchService: DataPublicPluginStart['search']; + readonly injectedMetadata: CoreStart['injectedMetadata']; } /** @internal **/ diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index c56060bb9c288..6eb4f82a940b1 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -27,6 +27,7 @@ import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../co import { getTitle } from '../../index_patterns/lib'; export type IndexPatternSelectProps = Required< + // Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'append' | 'prepend' | 'sortMatchesBy'>, Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder' > & { diff --git a/src/plugins/data/server/field_formats/field_formats_service.ts b/src/plugins/data/server/field_formats/field_formats_service.ts index 0dac64fb5dc1d..3404fe8cee9fd 100644 --- a/src/plugins/data/server/field_formats/field_formats_service.ts +++ b/src/plugins/data/server/field_formats/field_formats_service.ts @@ -17,16 +17,20 @@ * under the License. */ import { has } from 'lodash'; -import { FieldFormatsRegistry, IFieldFormatType, baseFormatters } from '../../common/field_formats'; +import { + FieldFormatsRegistry, + FieldFormatInstanceType, + baseFormatters, +} from '../../common/field_formats'; import { IUiSettingsClient } from '../../../../core/server'; import { DateFormat } from './converters'; export class FieldFormatsService { - private readonly fieldFormatClasses: IFieldFormatType[] = [DateFormat, ...baseFormatters]; + private readonly fieldFormatClasses: FieldFormatInstanceType[] = [DateFormat, ...baseFormatters]; public setup() { return { - register: (customFieldFormat: IFieldFormatType) => + register: (customFieldFormat: FieldFormatInstanceType) => this.fieldFormatClasses.push(customFieldFormat), }; } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts new file mode 100644 index 0000000000000..784b0b4d4f3d7 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values'; + +describe('shouldReadFieldFromDocValues', () => { + test('should read field from doc values for aggregatable "number" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'number')).toBe(true); + }); + + test('should not read field from doc values for non-aggregatable "number "field', async () => { + expect(shouldReadFieldFromDocValues(false, 'number')).toBe(false); + }); + + test('should not read field from doc values for "text" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'text')).toBe(false); + }); + + test('should not read field from doc values for "geo_shape" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'geo_shape')).toBe(false); + }); + + test('should not read field from doc values for underscore field', async () => { + expect(shouldReadFieldFromDocValues(true, '_source')).toBe(false); + }); +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts index 6d58f7a02c134..56a1cf3ccd161 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts @@ -18,5 +18,5 @@ */ export function shouldReadFieldFromDocValues(aggregatable: boolean, esType: string) { - return aggregatable && esType !== 'text' && !esType.startsWith('_'); + return aggregatable && !['text', 'geo_shape'].includes(esType) && !esType.startsWith('_'); } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c41023eab6d20..9b673de60ca65 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -283,7 +283,7 @@ export interface FieldFormatConfig { export const fieldFormats: { FieldFormatsRegistry: typeof FieldFormatsRegistry; FieldFormat: typeof FieldFormat; - serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; @@ -609,7 +609,7 @@ export class Plugin implements Plugin_2 { // (undocumented) setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; diff --git a/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap index 9f48e6e57e0ff..afb541253d994 100644 --- a/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -8,12 +8,12 @@ exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" >
@@ -39,12 +39,12 @@ exports[`FieldName renders a number field by providing a field record, useShortD class="euiFlexItem euiFlexItem--flexGrowZero" >
@@ -70,12 +70,12 @@ exports[`FieldName renders a string field by providing fieldType and fieldName 1 class="euiFlexItem euiFlexItem--flexGrowZero" >
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index bdb7bfbddc308..5ee66f9d19ac0 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -47,7 +47,8 @@ export { EmbeddableOutput, EmbeddablePanel, EmbeddableRoot, - EmbeddableVisTriggerContext, + ValueClickTriggerContext, + RangeSelectTriggerContext, ErrorEmbeddable, IContainer, IEmbeddable, diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts index 5297cf6cd365c..636ce3e623c5b 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts @@ -31,6 +31,14 @@ test('has expected display name', () => { expect(action.getDisplayName({} as any)).toMatchInlineSnapshot(`"Apply filter to current view"`); }); +describe('getIconType()', () => { + test('returns "filter" icon', async () => { + const action = createFilterAction(); + const result = action.getIconType({} as any); + expect(result).toBe('filter'); + }); +}); + describe('isCompatible()', () => { test('when embeddable filters and filters exist, returns true', async () => { const action = createFilterAction(); diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts index 4680512fb81c8..1cdb5af00e748 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts @@ -42,6 +42,7 @@ export function createFilterAction(): ActionByType { return createAction({ type: ACTION_APPLY_FILTER, id: ACTION_APPLY_FILTER, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('embeddableApi.actions.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index e29302fd6cc13..da7be1eea199a 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -18,18 +18,34 @@ */ import { Trigger } from '../../../../ui_actions/public'; +import { KibanaDatatable } from '../../../../expressions'; import { IEmbeddable } from '..'; export interface EmbeddableContext { embeddable: IEmbeddable; } -export interface EmbeddableVisTriggerContext { +export interface ValueClickTriggerContext { embeddable?: IEmbeddable; timeFieldName?: string; data: { - e?: MouseEvent; - data: unknown; + data: Array<{ + table: Pick; + column: number; + row: number; + value: any; + }>; + negate?: boolean; + }; +} + +export interface RangeSelectTriggerContext { + embeddable?: IEmbeddable; + timeFieldName?: string; + data: { + table: KibanaDatatable; + column: number; + range: number[]; }; } diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx new file mode 100644 index 0000000000000..68f1cf2045efb --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx @@ -0,0 +1,73 @@ +/* + * 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. + */ + +import { HttpSetup } from 'kibana/public'; +import React, { createContext, useContext } from 'react'; + +import { useRequest } from '../../../public'; + +import { Error as CustomError } from './section_error'; + +import { Privileges } from '../types'; + +interface Authorization { + isLoading: boolean; + apiError: CustomError | null; + privileges: Privileges; +} + +const initialValue: Authorization = { + isLoading: true, + apiError: null, + privileges: { + hasAllPrivileges: true, + missingPrivileges: {}, + }, +}; + +export const AuthorizationContext = createContext(initialValue); + +export const useAuthorizationContext = () => { + const ctx = useContext(AuthorizationContext); + if (!ctx) { + throw new Error('AuthorizationContext can only be used inside of AuthorizationProvider!'); + } + return ctx; +}; + +interface Props { + privilegesEndpoint: string; + children: React.ReactNode; + httpClient: HttpSetup; +} + +export const AuthorizationProvider = ({ privilegesEndpoint, httpClient, children }: Props) => { + const { isLoading, error, data: privilegesData } = useRequest(httpClient, { + path: privilegesEndpoint, + method: 'get', + }); + + const value = { + isLoading, + privileges: isLoading ? { hasAllPrivileges: true, missingPrivileges: {} } : privilegesData, + apiError: error ? error : null, + } as Authorization; + + return {children}; +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts new file mode 100644 index 0000000000000..71be3cc6152ca --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +export { + AuthorizationProvider, + AuthorizationContext, + useAuthorizationContext, +} from './authorization_provider'; + +export { WithPrivileges } from './with_privileges'; + +export { NotAuthorizedSection } from './not_authorized_section'; + +export { Error, SectionError } from './section_error'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/not_authorized_section.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/not_authorized_section.tsx new file mode 100644 index 0000000000000..c35f674ef9ec4 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/not_authorized_section.tsx @@ -0,0 +1,30 @@ +/* + * 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. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +interface Props { + title: React.ReactNode; + message: React.ReactNode | string; +} + +export const NotAuthorizedSection = ({ title, message }: Props) => ( + {title}} body={

{message}

} /> +); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx new file mode 100644 index 0000000000000..3d56309adae97 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx @@ -0,0 +1,63 @@ +/* + * 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. + */ + +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import React, { Fragment } from 'react'; + +export interface Error { + error: string; + cause?: string[]; + message?: string; +} + +interface Props { + title: React.ReactNode; + error: Error; + actions?: JSX.Element; +} + +export const SectionError: React.FunctionComponent = ({ + title, + error, + actions, + ...rest +}) => { + const { + error: errorString, + cause, // wrapEsError() on the server adds a "cause" array + message, + } = error; + + return ( + + {cause ? message || errorString :

{message || errorString}

} + {cause && ( + + +
    + {cause.map((causeMsg, i) => ( +
  • {causeMsg}
  • + ))} +
+
+ )} + {actions ? actions : null} +
+ ); +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx new file mode 100644 index 0000000000000..8f4b2b976d141 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx @@ -0,0 +1,90 @@ +/* + * 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. + */ + +import { MissingPrivileges } from '../types'; + +import { useAuthorizationContext } from './authorization_provider'; + +interface Props { + /** + * Each required privilege must have the format "section.privilege". + * To indicate that *all* privileges from a section are required, we can use the asterix + * e.g. "index.*" + */ + privileges: string | string[]; + children: (childrenProps: { + isLoading: boolean; + hasPrivileges: boolean; + privilegesMissing: MissingPrivileges; + }) => JSX.Element; +} + +type Privilege = [string, string]; + +const toArray = (value: string | string[]): string[] => + Array.isArray(value) ? (value as string[]) : ([value] as string[]); + +export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { + const { isLoading, privileges } = useAuthorizationContext(); + + const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map(p => { + const [section, privilege] = p.split('.'); + if (!privilege) { + // Oh! we forgot to use the dot "." notation. + throw new Error('Required privilege must have the format "section.privilege"'); + } + return [section, privilege]; + }); + + const hasPrivileges = isLoading + ? false + : privilegesToArray.every(privilege => { + const [section, requiredPrivilege] = privilege; + if (!privileges.missingPrivileges[section]) { + // if the section does not exist in our missingPriviledges, everything is OK + return true; + } + if (privileges.missingPrivileges[section]!.length === 0) { + return true; + } + if (requiredPrivilege === '*') { + // If length > 0 and we require them all... KO + return false; + } + // If we require _some_ privilege, we make sure that the one + // we require is *not* in the missingPrivilege array + return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); + }); + + const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { + if (privilege === '*') { + acc[section] = privileges.missingPrivileges[section] || []; + } else if ( + privileges.missingPrivileges[section] && + privileges.missingPrivileges[section]!.includes(privilege) + ) { + const missing: string[] = acc[section] || []; + acc[section] = [...missing, privilege]; + } + + return acc; + }, {} as MissingPrivileges); + + return children({ isLoading, hasPrivileges, privilegesMissing }); +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts new file mode 100644 index 0000000000000..ad89052b3bb54 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +export { + WithPrivileges, + NotAuthorizedSection, + AuthorizationProvider, + AuthorizationContext, + SectionError, + Error, + useAuthorizationContext, +} from './components'; + +export { Privileges, MissingPrivileges } from './types'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts new file mode 100644 index 0000000000000..cdc2052122688 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export interface MissingPrivileges { + [key: string]: string[] | undefined; +} + +export interface Privileges { + hasAllPrivileges: boolean; + missingPrivileges: MissingPrivileges; +} diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts new file mode 100644 index 0000000000000..3a02c0d2694f3 --- /dev/null +++ b/src/plugins/es_ui_shared/public/authorization/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export * from '../../__packages_do_not_import__/authorization'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index a0371bf351193..7e5510d7c9c65 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -47,6 +47,18 @@ export { expandLiteralStrings, } from './console_lang'; +export { + AuthorizationContext, + AuthorizationProvider, + NotAuthorizedSection, + WithPrivileges, + Privileges, + MissingPrivileges, + SectionError, + Error, + useAuthorizationContext, +} from './authorization'; + /** dummy plugin, we just want esUiShared to have its own bundle */ export function plugin() { return new (class EsUiSharedPlugin { diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index d5b047924f599..1c4b44a946e62 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -4,5 +4,5 @@ "server": true, "ui": true, "requiredPlugins": ["data", "kibanaLegacy"], - "optionalPlugins": ["usage_collection", "telemetry"] + "optionalPlugins": ["usageCollection", "telemetry"] } diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index d2f2d7041024e..1f530bc58f0b9 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -26,9 +26,10 @@ import { SampleDataRegistryStart, } from './services'; import { UsageCollectionSetup } from '../../usage_collection/server'; +import { sampleDataTelemetry } from './saved_objects'; interface HomeServerPluginSetupDependencies { - usage_collection?: UsageCollectionSetup; + usageCollection?: UsageCollectionSetup; } export class HomeServerPlugin implements Plugin { @@ -37,9 +38,10 @@ export class HomeServerPlugin implements Plugin { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx rename to src/plugins/input_control_vis/public/components/editor/options_tab.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 97850879a2d38..b6b852bcfa707 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -24,11 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { - IIndexPattern, - IFieldType, - IndexPatternSelect, -} from '../../../../../../plugins/data/public'; +import { IIndexPattern, IFieldType, IndexPatternSelect } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface RangeControlEditorProps { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap index 59ae99260cecd..43e2af6d099e8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap +++ b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap @@ -46,6 +46,7 @@ exports[`renders ListControl 1`] = ` placeholder="Select..." selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> `; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss b/src/plugins/input_control_vis/public/components/vis/_index.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss rename to src/plugins/input_control_vis/public/components/vis/_index.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss b/src/plugins/input_control_vis/public/components/vis/_vis.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss rename to src/plugins/input_control_vis/public/components/vis/_vis.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index 1712f024f5b7b..b0b674ad7b6ee 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -28,8 +28,6 @@ import { InputControlVis } from './input_control_vis'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const mockListControl: ListControl = { id: 'mock-list-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx index e2497287f35d0..c0ef99664fdf8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -23,8 +23,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { CONTROL_TYPES } from '../../editor_utils'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -import { ListControl as ListControlComponent } from '../vis/list_control'; -import { RangeControl as RangeControlComponent } from '../vis/range_control'; +import { ListControl as ListControlComponent } from './list_control'; +import { RangeControl as RangeControlComponent } from './range_control'; function isListControl(control: RangeControl | ListControl): control is ListControl { return control.type === CONTROL_TYPES.LIST; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.test.tsx index 639616151a395..ff5d572fa21c4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx @@ -23,8 +23,6 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { RangeControl, ceilWithPrecision, floorWithPrecision } from './range_control'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const control: RangeControlClass = { id: 'mock-range-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.tsx index 0cd2a2b331980..f028feaf5f84f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.tsx @@ -19,7 +19,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; -import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; +import { ValidatedDualRange } from '../../../../kibana_react/public'; import { FormRow } from './form_row'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts b/src/plugins/input_control_vis/public/control/control.test.ts similarity index 94% rename from src/legacy/core_plugins/input_control_vis/public/control/control.test.ts rename to src/plugins/input_control_vis/public/control/control.test.ts index e76b199a0262c..a2d220c14a3f7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts +++ b/src/plugins/input_control_vis/public/control/control.test.ts @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import { Control } from './control'; import { ControlParams } from '../editor_utils'; import { FilterManager as BaseFilterManager } from './filter_manager/filter_manager'; -import { SearchSource } from '../legacy_imports'; function createControlParams(id: string, label: string): ControlParams { return { @@ -51,18 +50,12 @@ class ControlMock extends Control { destroy() {} } -const mockKbnApi: SearchSource = {} as SearchSource; describe('hasChanged', () => { let control: ControlMock; beforeEach(() => { - control = new ControlMock( - createControlParams('3', 'control'), - mockFilterManager, - false, - mockKbnApi - ); + control = new ControlMock(createControlParams('3', 'control'), mockFilterManager, false); }); afterEach(() => { @@ -93,20 +86,17 @@ describe('ancestors', () => { grandParentControl = new ControlMock( createControlParams('1', 'grandparent control'), mockFilterManager, - false, - mockKbnApi + false ); parentControl = new ControlMock( createControlParams('2', 'parent control'), mockFilterManager, - false, - mockKbnApi + false ); childControl = new ControlMock( createControlParams('3', 'child control'), mockFilterManager, - false, - mockKbnApi + false ); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.ts b/src/plugins/input_control_vis/public/control/control.ts similarity index 95% rename from src/legacy/core_plugins/input_control_vis/public/control/control.ts rename to src/plugins/input_control_vis/public/control/control.ts index 6fddef777f73e..c57b09a19ebc8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.ts +++ b/src/plugins/input_control_vis/public/control/control.ts @@ -22,8 +22,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Filter } from '../../../../../plugins/data/public'; -import { SearchSource as SearchSourceClass } from '../legacy_imports'; +import { Filter } from 'src/plugins/data/public'; import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; @@ -61,8 +60,7 @@ export abstract class Control { constructor( public controlParams: ControlParams, public filterManager: FilterManager, - public useTimeFilter: boolean, - public SearchSource: SearchSourceClass + public useTimeFilter: boolean ) { this.id = controlParams.id; this.controlParams = controlParams; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts b/src/plugins/input_control_vis/public/control/control_factory.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts rename to src/plugins/input_control_vis/public/control/control_factory.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/plugins/input_control_vis/public/control/create_search_source.ts similarity index 83% rename from src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts rename to src/plugins/input_control_vis/public/control/create_search_source.ts index f238a2287ecdb..d6772a7cba5b8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/plugins/input_control_vis/public/control/create_search_source.ts @@ -17,11 +17,16 @@ * under the License. */ -import { PhraseFilter, IndexPattern, TimefilterContract } from '../../../../../plugins/data/public'; -import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; +import { + SearchSourceFields, + PhraseFilter, + IndexPattern, + TimefilterContract, + DataPublicPluginStart, +} from 'src/plugins/data/public'; export function createSearchSource( - SearchSource: SearchSourceClass, + { create }: DataPublicPluginStart['search']['searchSource'], initialState: SearchSourceFields | null, indexPattern: IndexPattern, aggs: any, @@ -29,7 +34,8 @@ export function createSearchSource( filters: PhraseFilter[] = [], timefilter: TimefilterContract ) { - const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); + const searchSource = create(initialState || {}); + // Do not not inherit from rootSearchSource to avoid picking up time and globals searchSource.setParent(undefined); searchSource.setField('filter', () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index 39c9d843e6bce..a9b7550be44ae 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -20,12 +20,8 @@ import expect from '@kbn/expect'; import { FilterManager } from './filter_manager'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; const setupMock = coreMock.createSetup(); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index 90b88a56950e2..bb806b336c3e0 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -19,11 +19,7 @@ import _ from 'lodash'; -import { - FilterManager as QueryFilterManager, - IndexPattern, - Filter, -} from '../../../../../../plugins/data/public'; +import { FilterManager as QueryFilterManager, IndexPattern, Filter } from '../../../../data/public'; export abstract class FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index 5be5d0157541e..6398c10b63a8c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -19,11 +19,7 @@ import expect from '@kbn/expect'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; import { PhraseFilterManager } from './phrase_filter_manager'; describe('PhraseFilterManager', function() { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 6f4a95b491907..bf167afa69bcf 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -25,7 +25,7 @@ import { esFilters, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; export class PhraseFilterManager extends FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index c776042ea4ba6..6e66b6942e5d3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -25,7 +25,7 @@ import { RangeFilterMeta, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; describe('RangeFilterManager', function() { const controlId = 'control1'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts similarity index 95% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 7a6719e85961b..1a884cf267c41 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -20,12 +20,7 @@ import _ from 'lodash'; import { FilterManager } from './filter_manager'; -import { - esFilters, - RangeFilter, - RangeFilterParams, - IFieldType, -} from '../../../../../../plugins/data/public'; +import { esFilters, RangeFilter, RangeFilterParams, IFieldType } from '../../../../data/public'; interface SliderValue { min?: string | number; diff --git a/src/plugins/input_control_vis/public/control/list_control_factory.test.ts b/src/plugins/input_control_vis/public/control/list_control_factory.test.ts new file mode 100644 index 0000000000000..72070175a233c --- /dev/null +++ b/src/plugins/input_control_vis/public/control/list_control_factory.test.ts @@ -0,0 +1,142 @@ +/* + * 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. + */ + +import { listControlFactory, ListControl } from './list_control_factory'; +import { ControlParams, CONTROL_TYPES } from '../editor_utils'; +import { getDepsMock, getSearchSourceMock } from '../test_utils'; + +describe('listControlFactory', () => { + const searchSourceMock = getSearchSourceMock(); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + describe('hasValue', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + const useTimeFilter = false; + + let listControl: ListControl; + beforeEach(async () => { + listControl = await listControlFactory(controlParams, useTimeFilter, deps); + }); + + test('should be false when control has no value', () => { + expect(listControl.hasValue()).toBe(false); + }); + + test('should be true when control has value', () => { + listControl.set([{ value: 'selected option', label: 'selection option' }]); + expect(listControl.hasValue()).toBe(true); + }); + + test('should be true when control has value that is the string "false"', () => { + listControl.set([{ value: 'false', label: 'selection option' }]); + expect(listControl.hasValue()).toBe(true); + }); + }); + + describe('fetch', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + const useTimeFilter = false; + + let listControl: ListControl; + beforeEach(async () => { + listControl = await listControlFactory(controlParams, useTimeFilter, deps); + }); + + test('should pass in timeout parameters from injected vars', async () => { + await listControl.fetch(); + expect(searchSourceMock).toHaveBeenCalledWith({ + timeout: `1000ms`, + terminate_after: 100000, + }); + }); + + test('should set selectOptions to results of terms aggregation', async () => { + await listControl.fetch(); + expect(listControl.selectOptions).toEqual([ + 'Zurich Airport', + 'Xi an Xianyang International Airport', + ]); + }); + }); + + describe('fetch with ancestors', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + const useTimeFilter = false; + + let listControl: ListControl; + let parentControl; + beforeEach(async () => { + listControl = await listControlFactory(controlParams, useTimeFilter, deps); + + const parentControlParams: ControlParams = { + id: 'parent', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + parentControl = await listControlFactory(parentControlParams, useTimeFilter, deps); + parentControl.clear(); + listControl.setAncestors([parentControl]); + }); + + describe('ancestor does not have value', () => { + test('should disable control', async () => { + await listControl.fetch(); + expect(listControl.isEnabled()).toBe(false); + }); + + test('should reset lastAncestorValues', async () => { + listControl.lastAncestorValues = 'last ancestor value'; + await listControl.fetch(); + expect(listControl.lastAncestorValues).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts b/src/plugins/input_control_vis/public/control/list_control_factory.ts similarity index 94% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts rename to src/plugins/input_control_vis/public/control/list_control_factory.ts index 8364c82efecdb..123ef83277e0b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/list_control_factory.ts @@ -19,14 +19,17 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; - -import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; +import { + IFieldType, + TimefilterContract, + SearchSourceFields, + DataPublicPluginStart, +} from 'src/plugins/data/public'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterContract } from '../../../../../plugins/data/public'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators @@ -75,6 +78,7 @@ const termsAgg = ({ field, size, direction, query }: TermsAggArgs) => { export class ListControl extends Control { private getInjectedVar: InputControlVisDependencies['core']['injectedMetadata']['getInjectedVar']; private timefilter: TimefilterContract; + private searchSource: DataPublicPluginStart['search']['searchSource']; abortController?: AbortController; lastAncestorValues: any; @@ -86,12 +90,13 @@ export class ListControl extends Control { controlParams: ControlParams, filterManager: PhraseFilterManager, useTimeFilter: boolean, - SearchSource: SearchSourceClass, + searchSource: DataPublicPluginStart['search']['searchSource'], deps: InputControlVisDependencies ) { - super(controlParams, filterManager, useTimeFilter, SearchSource); + super(controlParams, filterManager, useTimeFilter); this.getInjectedVar = deps.core.injectedMetadata.getInjectedVar; this.timefilter = deps.data.query.timefilter.timefilter; + this.searchSource = searchSource; } fetch = async (query?: string) => { @@ -143,7 +148,7 @@ export class ListControl extends Control { query, }); const searchSource = createSearchSource( - this.SearchSource, + this.searchSource, initialSearchSourceState, indexPattern, aggs, @@ -202,7 +207,6 @@ export class ListControl extends Control { export async function listControlFactory( controlParams: ControlParams, useTimeFilter: boolean, - SearchSource: SearchSourceClass, deps: InputControlVisDependencies ) { const [, { data: dataPluginStart }] = await deps.core.getStartServices(); @@ -225,7 +229,7 @@ export async function listControlFactory( deps.data.query.filterManager ), useTimeFilter, - SearchSource, + dataPluginStart.search.searchSource, deps ); return listControl; diff --git a/src/plugins/input_control_vis/public/control/range_control_factory.test.ts b/src/plugins/input_control_vis/public/control/range_control_factory.test.ts new file mode 100644 index 0000000000000..084c02e138a2d --- /dev/null +++ b/src/plugins/input_control_vis/public/control/range_control_factory.test.ts @@ -0,0 +1,97 @@ +/* + * 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. + */ + +import { rangeControlFactory } from './range_control_factory'; +import { ControlParams, CONTROL_TYPES } from '../editor_utils'; +import { getDepsMock, getSearchSourceMock } from '../test_utils'; + +describe('rangeControlFactory', () => { + describe('fetch', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myNumberField', + options: {}, + type: CONTROL_TYPES.RANGE, + label: 'test', + indexPattern: {} as any, + parent: {} as any, + }; + const useTimeFilter = false; + + test('should set min and max from aggregation results', async () => { + const esSearchResponse = { + aggregations: { + maxAgg: { value: 100 }, + minAgg: { value: 10 }, + }, + }; + const searchSourceMock = getSearchSourceMock(esSearchResponse); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + const rangeControl = await rangeControlFactory(controlParams, useTimeFilter, deps); + await rangeControl.fetch(); + + expect(rangeControl.isEnabled()).toBe(true); + expect(rangeControl.min).toBe(10); + expect(rangeControl.max).toBe(100); + }); + + test('should disable control when there are 0 hits', async () => { + // ES response when the query does not match any documents + const esSearchResponse = { + aggregations: { + maxAgg: { value: null }, + minAgg: { value: null }, + }, + }; + const searchSourceMock = getSearchSourceMock(esSearchResponse); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + const rangeControl = await rangeControlFactory(controlParams, useTimeFilter, deps); + await rangeControl.fetch(); + + expect(rangeControl.isEnabled()).toBe(false); + }); + + test('should disable control when response is empty', async () => { + // ES response for dashboardonly user who does not have read permissions on index is 200 (which is weird) + // and there is not aggregations key + const esSearchResponse = {}; + const searchSourceMock = getSearchSourceMock(esSearchResponse); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + const rangeControl = await rangeControlFactory(controlParams, useTimeFilter, deps); + await rangeControl.fetch(); + + expect(rangeControl.isEnabled()).toBe(false); + }); + }); +}); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts b/src/plugins/input_control_vis/public/control/range_control_factory.ts similarity index 91% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts rename to src/plugins/input_control_vis/public/control/range_control_factory.ts index d9b43c9dff201..326756ad5ffc6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/range_control_factory.ts @@ -20,13 +20,12 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { SearchSource as SearchSourceClass } from '../legacy_imports'; +import { IFieldType, TimefilterContract, DataPublicPluginStart } from 'src/plugins/data/public'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterContract } from '../.../../../../../../plugins/data/public'; const minMaxAgg = (field?: IFieldType) => { const aggBody: any = {}; @@ -52,6 +51,8 @@ const minMaxAgg = (field?: IFieldType) => { }; export class RangeControl extends Control { + private searchSource: DataPublicPluginStart['search']['searchSource']; + timefilter: TimefilterContract; abortController: any; min: any; @@ -61,11 +62,12 @@ export class RangeControl extends Control { controlParams: ControlParams, filterManager: RangeFilterManager, useTimeFilter: boolean, - SearchSource: SearchSourceClass, + searchSource: DataPublicPluginStart['search']['searchSource'], deps: InputControlVisDependencies ) { - super(controlParams, filterManager, useTimeFilter, SearchSource); + super(controlParams, filterManager, useTimeFilter); this.timefilter = deps.data.query.timefilter.timefilter; + this.searchSource = searchSource; } async fetch() { @@ -83,7 +85,7 @@ export class RangeControl extends Control { const fieldName = this.filterManager.fieldName; const aggs = minMaxAgg(indexPattern.fields.getByName(fieldName)); const searchSource = createSearchSource( - this.SearchSource, + this.searchSource, null, indexPattern, aggs, @@ -129,7 +131,6 @@ export class RangeControl extends Control { export async function rangeControlFactory( controlParams: ControlParams, useTimeFilter: boolean, - SearchSource: SearchSourceClass, deps: InputControlVisDependencies ): Promise { const [, { data: dataPluginStart }] = await deps.core.getStartServices(); @@ -144,7 +145,7 @@ export async function rangeControlFactory( deps.data.query.filterManager ), useTimeFilter, - SearchSource, + dataPluginStart.search.searchSource, deps ); } diff --git a/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts b/src/plugins/input_control_vis/public/editor_utils.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/editor_utils.ts rename to src/plugins/input_control_vis/public/editor_utils.ts diff --git a/src/plugins/input_control_vis/public/index.scss b/src/plugins/input_control_vis/public/index.scss new file mode 100644 index 0000000000000..42fded23d7761 --- /dev/null +++ b/src/plugins/input_control_vis/public/index.scss @@ -0,0 +1,9 @@ +// Prefix all styles with "icv" to avoid conflicts. +// Examples +// icvChart +// icvChart__legend +// icvChart__legend--small +// icvChart__legend-isLoading + +@import './components/editor/index'; +@import './components/vis/index'; diff --git a/src/plugins/input_control_vis/public/index.ts b/src/plugins/input_control_vis/public/index.ts new file mode 100644 index 0000000000000..8edd3fd9996c3 --- /dev/null +++ b/src/plugins/input_control_vis/public/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +import './index.scss'; + +import { PluginInitializerContext } from '../../../core/public'; +import { InputControlVisPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts b/src/plugins/input_control_vis/public/input_control_fn.test.ts similarity index 86% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts rename to src/plugins/input_control_vis/public/input_control_fn.test.ts index acc214ed31180..f3ea2d2d6f0ba 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.test.ts @@ -18,11 +18,7 @@ */ import { createInputControlVisFn } from './input_control_fn'; - -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; - -jest.mock('./legacy_imports.ts'); +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#input_control_vis', () => { const fn = functionWrapper(createInputControlVisFn()); @@ -48,8 +44,9 @@ describe('interpreter/functions#input_control_vis', () => { pinFilters: false, }; - it('returns an object with the correct structure', async () => { + test('returns an object with the correct structure', async () => { const actual = await fn(null, { visConfig: JSON.stringify(visConfig) }); + expect(actual).toMatchSnapshot(); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts rename to src/plugins/input_control_vis/public/input_control_fn.ts index e779c6d344ab5..59c0e03505bb7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; interface Arguments { visConfig: string; diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts similarity index 96% rename from src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts rename to src/plugins/input_control_vis/public/input_control_vis_type.ts index badea68eec19f..8114dbf110f8b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -23,7 +23,7 @@ import { createInputControlVisController } from './vis_controller'; import { getControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; import { InputControlVisDependencies } from './plugin'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { const InputControlVisController = createInputControlVisController(deps); diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/index.ts b/src/plugins/input_control_vis/public/lineage/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/index.ts rename to src/plugins/input_control_vis/public/lineage/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.ts diff --git a/src/plugins/input_control_vis/public/plugin.ts b/src/plugins/input_control_vis/public/plugin.ts new file mode 100644 index 0000000000000..9fc7df24c2dcc --- /dev/null +++ b/src/plugins/input_control_vis/public/plugin.ts @@ -0,0 +1,70 @@ +/* + * 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. + */ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; + +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { createInputControlVisFn } from './input_control_fn'; +import { createInputControlVisTypeDefinition } from './input_control_vis_type'; + +type InputControlVisCoreSetup = CoreSetup; + +export interface InputControlVisDependencies { + core: InputControlVisCoreSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginStartDependencies { + expressions: ReturnType; + visualizations: VisualizationsStart; + data: DataPublicPluginStart; +} + +/** @internal */ +export class InputControlVisPlugin implements Plugin { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: InputControlVisCoreSetup, + { expressions, visualizations, data }: InputControlVisPluginSetupDependencies + ) { + const visualizationDependencies: Readonly = { + core, + data, + }; + + expressions.registerFunction(createInputControlVisFn); + visualizations.createBaseVisualization( + createInputControlVisTypeDefinition(visualizationDependencies) + ); + } + + public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) { + // nothing to do here + } +} diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx b/src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx similarity index 85% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx rename to src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx index 78a4ef3a5597a..efd7d4c020854 100644 --- a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx +++ b/src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { InputControlVisDependencies } from '../plugin'; +import { getSearchSourceMock } from './get_search_service_mock'; const fields = [] as any; fields.push({ name: 'myField' } as any); @@ -26,13 +27,20 @@ fields.getByName = (name: any) => { return fields.find(({ name: n }: { name: string }) => n === name); }; -export const getDepsMock = (): InputControlVisDependencies => +export const getDepsMock = ({ + searchSource = { + create: getSearchSourceMock(), + }, +} = {}): InputControlVisDependencies => ({ core: { getStartServices: jest.fn().mockReturnValue([ null, { data: { + search: { + searchSource, + }, ui: { IndexPatternSelect: () => (
) as any, }, @@ -58,6 +66,11 @@ export const getDepsMock = (): InputControlVisDependencies => }, }, data: { + search: { + searchSource: { + create: getSearchSourceMock(), + }, + }, query: { filterManager: { fieldName: 'myField', diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts similarity index 91% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts index 94a460086e9da..24b7d7bcbb5c1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts +++ b/src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts @@ -17,9 +17,7 @@ * under the License. */ -import { SearchSource } from '../legacy_imports'; - -export const getSearchSourceMock = (esSearchResponse?: any): SearchSource => +export const getSearchSourceMock = (esSearchResponse?: any) => jest.fn().mockImplementation(() => ({ setParent: jest.fn(), setField: jest.fn(), diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts b/src/plugins/input_control_vis/public/test_utils/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts rename to src/plugins/input_control_vis/public/test_utils/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts b/src/plugins/input_control_vis/public/test_utils/update_component.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts rename to src/plugins/input_control_vis/public/test_utils/update_component.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx rename to src/plugins/input_control_vis/public/vis_controller.tsx index c4a7d286850e3..97506556d7e0a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -21,8 +21,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nStart } from 'kibana/public'; -import { SearchSource } from './legacy_imports'; - import { InputControlVis } from './components/vis/input_control_vis'; import { getControlFactory } from './control/control_factory'; import { getLineageMap } from './lineage'; @@ -30,8 +28,8 @@ import { ControlParams } from './editor_utils'; import { RangeControl } from './control/range_control_factory'; import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; -import { FilterManager, Filter } from '../../../../plugins/data/public'; -import { VisParams, Vis } from '../../../../plugins/visualizations/public'; +import { FilterManager, Filter } from '../../data/public'; +import { VisParams, Vis } from '../../visualizations/public'; export const createInputControlVisController = (deps: InputControlVisDependencies) => { return class InputControlVisController { @@ -102,7 +100,8 @@ export const createInputControlVisController = (deps: InputControlVisDependencie const controlFactoryPromises = controlParamsList.map(controlParams => { const factory = getControlFactory(controlParams); - return factory(controlParams, this.visParams?.useTimeFilter, SearchSource, deps); + + return factory(controlParams, this.visParams?.useTimeFilter, deps); }); const controls = await Promise.all(controlFactoryPromises); diff --git a/src/plugins/input_control_vis/server/index.ts b/src/plugins/input_control_vis/server/index.ts new file mode 100644 index 0000000000000..043657ba98a3c --- /dev/null +++ b/src/plugins/input_control_vis/server/index.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export const config: PluginConfigDescriptor = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index 71cd57ef2d72e..295cf27688c80 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -92,9 +92,9 @@ export const configureAppAngularModule = ( ) => { const core = 'core' in newPlatform ? newPlatform.core : newPlatform; const packageInfo = - 'injectedMetadata' in newPlatform - ? newPlatform.injectedMetadata.getLegacyMetadata() - : newPlatform.env.packageInfo; + 'env' in newPlatform + ? newPlatform.env.packageInfo + : newPlatform.injectedMetadata.getLegacyMetadata(); if ('injectedMetadata' in newPlatform) { forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { diff --git a/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx b/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx deleted file mode 100644 index 7992f650cb372..0000000000000 --- a/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx +++ /dev/null @@ -1,98 +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. - */ - -import { contains } from 'lodash'; -import React from 'react'; -import { History } from 'history'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut } from '@elastic/eui'; -import { CoreStart } from 'kibana/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { toMountPoint } from '../../../kibana_react/public'; - -let bannerId: string; -let timeoutId: NodeJS.Timeout | undefined; - -/** - * Checks whether a default index pattern is set and exists and defines - * one otherwise. - * - * If there are no index patterns, redirect to management page and show - * banner. In this case the promise returned from this function will never - * resolve to wait for the URL change to happen. - */ -export async function ensureDefaultIndexPattern( - core: CoreStart, - data: DataPublicPluginStart, - history: History -) { - const patterns = await data.indexPatterns.getIds(); - let defaultId = core.uiSettings.get('defaultIndex'); - let defined = !!defaultId; - const exists = contains(patterns, defaultId); - - if (defined && !exists) { - core.uiSettings.remove('defaultIndex'); - defaultId = defined = false; - } - - if (defined) { - return; - } - - // If there is any index pattern created, set the first as default - if (patterns.length >= 1) { - defaultId = patterns[0]; - core.uiSettings.set('defaultIndex', defaultId); - } else { - const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; - const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - // Avoid being hostile to new users who don't have an index pattern setup yet - // give them a friendly info message instead of a terse error message - bannerId = core.overlays.banners.replace( - bannerId, - toMountPoint( - - ) - ); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - timeoutId = setTimeout(() => { - core.overlays.banners.remove(bannerId); - timeoutId = undefined; - }, 15000); - - history.push(redirectTarget); - - // return never-resolving promise to stop resolving and wait for the url change - return new Promise(() => {}); - } -} diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index 1a73bbb6b04a1..bb13ea09f928a 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -19,4 +19,3 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; -export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 2f139050e994a..c634322b23d0b 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -74,7 +74,7 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; +export { removeQueryParam, redirectWhenMissing } from './history'; export { applyDiff } from './state_management/utils/diff_object'; /** dummy plugin, we just want kibanaUtils to have its own bundle */ diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index 6e4c505c62ebc..513c70e60048a 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -31,6 +31,7 @@ import { setStateToKbnUrl, getStateFromKbnUrl, } from './kbn_url_storage'; +import { ScopedHistory } from '../../../../../core/public'; describe('kbn_url_storage', () => { describe('getStateFromUrl & setStateToUrl', () => { @@ -187,23 +188,54 @@ describe('kbn_url_storage', () => { urlControls.update('/', true); }); - const getCurrentUrl = () => window.location.href; + const getCurrentUrl = () => history.createHref(history.location); it('should flush async url updates', async () => { const pr1 = urlControls.updateAsync(() => '/1', false); const pr2 = urlControls.updateAsync(() => '/2', false); const pr3 = urlControls.updateAsync(() => '/3', false); - expect(getCurrentUrl()).toBe('http://localhost/'); - expect(urlControls.flush()).toBe('http://localhost/3'); - expect(getCurrentUrl()).toBe('http://localhost/3'); + expect(getCurrentUrl()).toBe('/'); + expect(urlControls.flush()).toBe('/3'); + expect(getCurrentUrl()).toBe('/3'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + }); + + it('flush() should return undefined, if no url updates happened', () => { + expect(urlControls.flush()).toBeUndefined(); + urlControls.updateAsync(() => '/1', false); + urlControls.updateAsync(() => '/', false); + expect(urlControls.flush()).toBeUndefined(); + }); + }); + + describe('urlControls - scoped history integration', () => { + let history: History; + let urlControls: IKbnUrlControls; + beforeEach(() => { + const parentHistory = createBrowserHistory(); + parentHistory.replace('/app/kibana/'); + history = new ScopedHistory(parentHistory, '/app/kibana/'); + urlControls = createKbnUrlControls(history); + }); + + const getCurrentUrl = () => history.createHref(history.location); + + it('should flush async url updates', async () => { + const pr1 = urlControls.updateAsync(() => '/app/kibana/1', false); + const pr2 = urlControls.updateAsync(() => '/app/kibana/2', false); + const pr3 = urlControls.updateAsync(() => '/app/kibana/3', false); + expect(getCurrentUrl()).toBe('/app/kibana/'); + expect(urlControls.flush()).toBe('/app/kibana/3'); + expect(getCurrentUrl()).toBe('/app/kibana/3'); await Promise.all([pr1, pr2, pr3]); - expect(getCurrentUrl()).toBe('http://localhost/3'); + expect(getCurrentUrl()).toBe('/app/kibana/3'); }); it('flush() should return undefined, if no url updates happened', () => { expect(urlControls.flush()).toBeUndefined(); - urlControls.updateAsync(() => 'http://localhost/1', false); - urlControls.updateAsync(() => 'http://localhost/', false); + urlControls.updateAsync(() => '/app/kibana/1', false); + urlControls.updateAsync(() => '/app/kibana/', false); expect(urlControls.flush()).toBeUndefined(); }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 40a411d425a54..337d122e2854b 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -154,7 +154,7 @@ export const createKbnUrlControls = ( let shouldReplace = true; function updateUrl(newUrl: string, replace = false): string | undefined { - const currentUrl = getCurrentUrl(); + const currentUrl = getCurrentUrl(history); if (newUrl === currentUrl) return undefined; // skip update const historyPath = getRelativeToHistoryPath(newUrl, history); @@ -165,7 +165,7 @@ export const createKbnUrlControls = ( history.push(historyPath); } - return getCurrentUrl(); + return getCurrentUrl(history); } // queue clean up @@ -187,7 +187,10 @@ export const createKbnUrlControls = ( function getPendingUrl() { if (updateQueue.length === 0) return undefined; - const resultUrl = updateQueue.reduce((url, nextUpdate) => nextUpdate(url), getCurrentUrl()); + const resultUrl = updateQueue.reduce( + (url, nextUpdate) => nextUpdate(url), + getCurrentUrl(history) + ); return resultUrl; } diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts index af8811b1969e6..8adbbfb06e1ed 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts @@ -57,6 +57,7 @@ export function createKbnUrlTracker({ navLinkUpdater$, toastNotifications, history, + getHistory, storage, shouldTrackUrlUpdate = pathname => { const currentAppName = defaultSubUrl.slice(2); // cut hash and slash symbols @@ -103,6 +104,12 @@ export function createKbnUrlTracker({ * History object to use to track url changes. If this isn't provided, a local history instance will be created. */ history?: History; + + /** + * Lazily retrieve history instance + */ + getHistory?: () => History; + /** * Storage object to use to persist currently active url. If this isn't provided, the browser wide session storage instance will be used. */ @@ -158,7 +165,7 @@ export function createKbnUrlTracker({ function onMountApp() { unsubscribe(); - const historyInstance = history || createHashHistory(); + const historyInstance = history || (getHistory && getHistory()) || createHashHistory(); // track current hash when within app unsubscribeURLHistory = historyInstance.listen(location => { if (shouldTrackUrlUpdate(location.pathname)) { diff --git a/src/plugins/kibana_utils/public/state_management/url/parse.ts b/src/plugins/kibana_utils/public/state_management/url/parse.ts index 95041d0662f56..6339002ea5c68 100644 --- a/src/plugins/kibana_utils/public/state_management/url/parse.ts +++ b/src/plugins/kibana_utils/public/state_management/url/parse.ts @@ -18,12 +18,11 @@ */ import { parse as _parseUrl } from 'url'; +import { History } from 'history'; export const parseUrl = (url: string) => _parseUrl(url, true); export const parseUrlHash = (url: string) => { const hash = parseUrl(url).hash; return hash ? parseUrl(hash.slice(1)) : null; }; -export const getCurrentUrl = () => window.location.href; -export const parseCurrentUrl = () => parseUrl(getCurrentUrl()); -export const parseCurrentUrlHash = () => parseUrlHash(getCurrentUrl()); +export const getCurrentUrl = (history: History) => history.createHref(history.location); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index cc3f1df7c1e00..8a9a4ea71ee9a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -21,6 +21,7 @@ import { createKbnUrlStateStorage, IKbnUrlStateStorage } from './create_kbn_url_ import { History, createBrowserHistory } from 'history'; import { takeUntil, toArray } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { ScopedHistory } from '../../../../../core/public'; describe('KbnUrlStateStorage', () => { describe('useHash: false', () => { @@ -132,4 +133,78 @@ describe('KbnUrlStateStorage', () => { expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); }); }); + + describe('ScopedHistory integration', () => { + let urlStateStorage: IKbnUrlStateStorage; + let history: ScopedHistory; + const getCurrentUrl = () => history.createHref(history.location); + beforeEach(() => { + const parentHistory = createBrowserHistory(); + parentHistory.push('/kibana/app/'); + history = new ScopedHistory(parentHistory, '/kibana/app/'); + urlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); + }); + + it('should persist state to url', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + await urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/#?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + }); + + it('should flush state to url', () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); + expect(urlStateStorage.flush()).toBe(true); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/#?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + + expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update + }); + + it('should cancel url updates', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + const pr = urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); + urlStateStorage.cancel(); + await pr; + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); + expect(urlStateStorage.get(key)).toEqual(null); + }); + + it('should cancel url updates if synchronously returned to the same state', async () => { + const state1 = { test: 'test', ok: 1 }; + const state2 = { test: 'test', ok: 2 }; + const key = '_s'; + const pr1 = urlStateStorage.set(key, state1); + await pr1; + const historyLength = history.length; + const pr2 = urlStateStorage.set(key, state2); + const pr3 = urlStateStorage.set(key, state1); + await Promise.all([pr2, pr3]); + expect(history.length).toBe(historyLength); + }); + + it('should notify about url changes', async () => { + expect(urlStateStorage.change$).toBeDefined(); + const key = '_s'; + const destroy$ = new Subject(); + const result = urlStateStorage.change$!(key) + .pipe(takeUntil(destroy$), toArray()) + .toPromise(); + + history.push(`/#?${key}=(ok:1,test:test)`); + history.push(`/?query=test#?${key}=(ok:2,test:test)&some=test`); + history.push(`/?query=test#?some=test`); + + destroy$.next(); + destroy$.complete(); + + expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); + }); + }); }); diff --git a/src/plugins/management/public/management_service.test.ts b/src/plugins/management/public/management_service.test.ts index ceb91837921eb..18569ef285ff3 100644 --- a/src/plugins/management/public/management_service.test.ts +++ b/src/plugins/management/public/management_service.test.ts @@ -29,9 +29,8 @@ test('Provides default sections', () => { () => {}, coreMock.createSetup().getStartServices ); - expect(service.getAllSections().length).toEqual(3); + expect(service.getAllSections().length).toEqual(2); expect(service.getSection('kibana')).not.toBeUndefined(); - expect(service.getSection('logstash')).not.toBeUndefined(); expect(service.getSection('elasticsearch')).not.toBeUndefined(); }); diff --git a/src/plugins/management/public/management_service.ts b/src/plugins/management/public/management_service.ts index ed31a22992da8..8fc207e32e6ce 100644 --- a/src/plugins/management/public/management_service.ts +++ b/src/plugins/management/public/management_service.ts @@ -80,7 +80,6 @@ export class ManagementService { ); register({ id: 'kibana', title: 'Kibana', order: 30, euiIconType: 'logoKibana' }); - register({ id: 'logstash', title: 'Logstash', order: 30, euiIconType: 'logoLogstash' }); register({ id: 'elasticsearch', title: 'Elasticsearch', diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss index 5befe4789dd6c..a6ddf7a8b4264 100644 --- a/src/plugins/navigation/public/top_nav_menu/_index.scss +++ b/src/plugins/navigation/public/top_nav_menu/_index.scss @@ -8,4 +8,8 @@ padding: 0 $euiSizeS; } } + + .kbnTopNavMenu-isFullScreen { + padding: 0; + } } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 8e0e8b3031132..74cfd125c2e3a 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -75,4 +75,17 @@ describe('TopNavMenu', () => { expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); }); + + it('Should render with a class name', () => { + const component = shallowWithIntl( + + ); + expect(component.find('.kbnTopNavMenu').length).toBe(1); + expect(component.find('.myCoolClass').length).toBeTruthy(); + }); }); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 14ad40f13e388..d492c7feb61a7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; @@ -29,6 +30,7 @@ export type TopNavMenuProps = StatefulSearchBarProps & { config?: TopNavMenuData[]; showSearchBar?: boolean; data?: DataPublicPluginStart; + className?: string; }; /* @@ -65,6 +67,7 @@ export function TopNavMenu(props: TopNavMenuProps) { } function renderLayout() { + const className = classNames('kbnTopNavMenu', props.className); return ( {renderItems()} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 9776887b6d741..df687a051fc7d 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -17,9 +17,9 @@ * under the License. */ import _ from 'lodash'; -import { EsResponse, SavedObject, SavedObjectConfig } from '../../types'; +import { EsResponse, SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from '../../types'; import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public'; -import { DataPublicPluginStart, IndexPattern } from '../../../../data/public'; +import { IndexPattern } from '../../../../data/public'; /** * A given response of and ElasticSearch containing a plain saved object is applied to the given @@ -29,7 +29,7 @@ export async function applyESResp( resp: EsResponse, savedObject: SavedObject, config: SavedObjectConfig, - createSearchSource: DataPublicPluginStart['search']['createSearchSource'] + dependencies: SavedObjectKibanaServices ) { const mapping = expandShorthand(config.mapping); const esType = config.type || ''; @@ -65,7 +65,10 @@ export async function applyESResp( if (config.searchSource) { try { - savedObject.searchSource = await createSearchSource(meta.searchSourceJSON, resp.references); + savedObject.searchSource = await dependencies.search.searchSource.fromJSON( + meta.searchSourceJSON, + resp.references + ); } catch (error) { if ( error.constructor.name === 'SavedObjectNotFound' && diff --git a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts index e8faef4e9e040..e5b0e18e7b433 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import _ from 'lodash'; -import { SearchSource } from '../../../../data/public'; +import { once } from 'lodash'; import { hydrateIndexPattern } from './hydrate_index_pattern'; import { intializeSavedObject } from './initialize_saved_object'; import { serializeSavedObject } from './serialize_saved_object'; @@ -55,7 +54,9 @@ export function buildSavedObject( savedObject.isSaving = false; savedObject.defaults = config.defaults || {}; // optional search source which this object configures - savedObject.searchSource = config.searchSource ? new SearchSource() : undefined; + savedObject.searchSource = config.searchSource + ? services.search.searchSource.create() + : undefined; // the id of the document savedObject.id = config.id || void 0; // the migration version of the document, should only be set on imports @@ -79,10 +80,9 @@ export function buildSavedObject( * @return {Promise} * @resolved {SavedObject} */ - savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); + savedObject.init = once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); - savedObject.applyESResp = (resp: EsResponse) => - applyESResp(resp, savedObject, config, services.search.createSearchSource); + savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config, services); /** * Serialize this object diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index 60c66f84080b2..f7e67dbe3ee1d 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -28,9 +28,8 @@ import { // @ts-ignore import StubIndexPattern from 'test_utils/stub_index_pattern'; -import { InvalidJSONProperty } from '../../../kibana_utils/public'; import { coreMock } from '../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../plugins/data/public/mocks'; +import { dataPluginMock, createSearchSourceMock } from '../../../../plugins/data/public/mocks'; import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; import { IIndexPattern } from '../../../data/common/index_patterns'; @@ -40,9 +39,9 @@ describe('Saved Object', () => { const startMock = coreMock.createStart(); const dataStartMock = dataPluginMock.createStartContract(); const saveOptionsMock = {} as SavedObjectSaveOpts; + const savedObjectsClientStub = startMock.savedObjects.client; let SavedObjectClass: new (config: SavedObjectConfig) => SavedObject; - const savedObjectsClientStub = startMock.savedObjects.client; /** * Returns a fake doc response with the given index and id, of type dashboard @@ -99,16 +98,22 @@ describe('Saved Object', () => { function createInitializedSavedObject(config: SavedObjectConfig = {}) { const savedObject = new SavedObjectClass(config); savedObject.title = 'my saved object'; + return savedObject.init!(); } beforeEach(() => { - (dataStartMock.search.createSearchSource as jest.Mock).mockReset(); - SavedObjectClass = createSavedObjectClass({ + SavedObjectClass = createSavedObjectClass(({ savedObjectsClient: savedObjectsClientStub, indexPatterns: dataStartMock.indexPatterns, - search: dataStartMock.search, - } as SavedObjectKibanaServices); + search: { + ...dataStartMock.search, + searchSource: { + ...dataStartMock.search.searchSource, + create: createSearchSourceMock, + }, + }, + } as unknown) as SavedObjectKibanaServices); }); describe('save', () => { @@ -411,27 +416,6 @@ describe('Saved Object', () => { }); }); - it('forwards thrown exceptions from createSearchSource', async () => { - (dataStartMock.search.createSearchSource as jest.Mock).mockImplementation(() => { - throw new InvalidJSONProperty(''); - }); - const savedObject = await createInitializedSavedObject({ - type: 'dashboard', - searchSource: true, - }); - const response = { - found: true, - _source: {}, - }; - - try { - await savedObject.applyESResp(response); - throw new Error('applyESResp should have failed, but did not.'); - } catch (err) { - expect(err instanceof InvalidJSONProperty).toBe(true); - } - }); - it('preserves original defaults if not overridden', () => { const id = 'anid'; const preserveMeValue = 'here to stay!'; @@ -589,42 +573,45 @@ describe('Saved Object', () => { it('passes references to search source parsing function', async () => { const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true }); - return savedObject.init!().then(() => { - const searchSourceJSON = JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - filter: [ - { - meta: { - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - }, - }, - ], - }); - const response = { - found: true, - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON, + await savedObject.init!(); + + const searchSourceJSON = JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + filter: [ + { + meta: { + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', }, }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'my-index-1', - }, - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - type: 'index-pattern', - id: 'my-index-2', - }, - ], - }; - savedObject.applyESResp(response); - expect(dataStartMock.search.createSearchSource).toBeCalledWith( - searchSourceJSON, - response.references - ); + ], + }); + const response = { + found: true, + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'my-index-1', + }, + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + id: 'my-index-2', + }, + ], + }; + const result = await savedObject.applyESResp(response); + + expect(result._source).toEqual({ + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index","filter":[{"meta":{"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"}}]}', + }, }); }); }); diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts index 23c2b75169555..f98b1762511f4 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts @@ -25,6 +25,7 @@ import { } from './resolve_saved_objects'; import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; import { IndexPatternsContract } from '../../../data/public'; +import { dataPluginMock } from '../../../data/public/mocks'; class SavedObjectNotFound extends Error { constructor(options: Record) { @@ -233,6 +234,19 @@ describe('resolveSavedObjects', () => { }); describe('resolveIndexPatternConflicts', () => { + let dependencies: Parameters[3]; + + beforeEach(() => { + const search = dataPluginMock.createStartContract().search; + + dependencies = { + indexPatterns: ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract, + search, + }; + }); + it('should resave resolutions', async () => { const save = jest.fn(); @@ -284,11 +298,13 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ - get: (id: string) => Promise.resolve({ id }), - } as unknown) as IndexPatternsContract); - expect(conflictedIndexPatterns[0].obj.searchSource!.getField('index')!.id).toEqual('2'); - expect(conflictedIndexPatterns[1].obj.searchSource!.getField('index')!.id).toEqual('4'); + await resolveIndexPatternConflicts( + resolutions, + conflictedIndexPatterns, + overwriteAll, + dependencies + ); + expect(save.mock.calls.length).toBe(2); expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); }); @@ -345,13 +361,13 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ - get: (id: string) => Promise.resolve({ id }), - } as unknown) as IndexPatternsContract); + await resolveIndexPatternConflicts( + resolutions, + conflictedIndexPatterns, + overwriteAll, + dependencies + ); - expect(conflictedIndexPatterns[0].obj.searchSource!.getField('filter')).toEqual([ - { meta: { index: 'newFilterIndex' } }, - ]); expect(save.mock.calls.length).toBe(2); }); }); diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts index 15e03ed39d88c..d4764b8949a60 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { cloneDeep } from 'lodash'; import { OverlayStart, SavedObjectReference } from 'src/core/public'; import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; -import { IndexPatternsContract, IIndexPattern, createSearchSource } from '../../../data/public'; +import { IndexPatternsContract, IIndexPattern, DataPublicPluginStart } from '../../../data/public'; type SavedObjectsRawDoc = Record; @@ -162,7 +162,10 @@ export async function resolveIndexPatternConflicts( resolutions: Array<{ oldId: string; newId: string }>, conflictedIndexPatterns: any[], overwriteAll: boolean, - indexPatterns: IndexPatternsContract + dependencies: { + indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; + } ) { let importCount = 0; @@ -208,7 +211,7 @@ export async function resolveIndexPatternConflicts( // The user decided to skip this conflict so do nothing return; } - obj.searchSource = await createSearchSource(indexPatterns)( + obj.searchSource = await dependencies.search.searchSource.fromJSON( JSON.stringify(serializedSearchSource), replacedReferences ); diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 6f03f97079bb6..fe3150fc0bb07 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -187,6 +187,7 @@ const SavedObjectsTablePage = ({ actionRegistry={actionRegistry} savedObjectsClient={coreStart.savedObjects.client} indexPatterns={dataStart.indexPatterns} + search={dataStart.search} http={coreStart.http} overlays={coreStart.overlays} notifications={coreStart.notifications} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index fe64df6ff51d1..fb3d6efa63826 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -238,6 +238,7 @@ exports[`SavedObjectsTable import should show the flyout 1`] = ` indexPatterns={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } @@ -256,6 +257,41 @@ exports[`SavedObjectsTable import should show the flyout 1`] = ` "openModal": [MockFunction], } } + search={ + Object { + "__LEGACY": Object { + "AggConfig": [MockFunction], + "AggType": [MockFunction], + "FieldParamType": [MockFunction], + "MetricAggType": [MockFunction], + "aggTypeFieldFilters": AggTypeFieldFilters { + "filters": Set {}, + }, + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + "parentPipelineAggHelper": [MockFunction], + "siblingPipelineAggHelper": [MockFunction], + }, + "aggs": Object { + "calculateAutoTimeExpression": [Function], + "createAggConfigs": [MockFunction], + "types": Object { + "get": [Function], + "getAll": [Function], + "getBuckets": [Function], + "getMetrics": [Function], + }, + }, + "search": [MockFunction], + "searchSource": Object { + "create": [MockFunction], + "fromJSON": [MockFunction], + }, + "setInterceptor": [MockFunction], + } + } serviceRegistry={ Object { "all": [MockFunction], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 5d713ff044f24..c915a8a2be8f8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -33,6 +33,7 @@ import { coreMock } from '../../../../../../core/public/mocks'; import { serviceRegistryMock } from '../../../services/service_registry.mock'; import { Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; +import { dataPluginMock } from '../../../../../data/public/mocks'; const mockFile = ({ name: 'foo.ndjson', @@ -56,6 +57,7 @@ describe('Flyout', () => { beforeEach(() => { const { http, overlays } = coreMock.createStart(); + const search = dataPluginMock.createStartContract().search; defaultProps = { close: jest.fn(), @@ -68,6 +70,7 @@ describe('Flyout', () => { http, allowedTypes: ['search', 'index-pattern', 'visualization'], serviceRegistry: serviceRegistryMock.create(), + search, }; }); @@ -499,7 +502,10 @@ describe('Flyout', () => { component.instance().resolutions, mockConflictedIndexPatterns, true, - defaultProps.indexPatterns + { + search: defaultProps.search, + indexPatterns: defaultProps.indexPatterns, + } ); expect(saveObjectsMock).toHaveBeenCalledWith( mockConflictedSavedObjectsLinkedToSavedSearches, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 45788dcb601ae..fbcfeafe291a3 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -48,7 +48,11 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { OverlayStart, HttpStart } from 'src/core/public'; -import { IndexPatternsContract, IIndexPattern } from '../../../../../data/public'; +import { + IndexPatternsContract, + IIndexPattern, + DataPublicPluginStart, +} from '../../../../../data/public'; import { importFile, importLegacyFile, @@ -75,6 +79,7 @@ export interface FlyoutProps { indexPatterns: IndexPatternsContract; overlays: OverlayStart; http: HttpStart; + search: DataPublicPluginStart['search']; } export interface FlyoutState { @@ -362,7 +367,7 @@ export class Flyout extends Component { failedImports, } = this.state; - const { serviceRegistry, indexPatterns } = this.props; + const { serviceRegistry, indexPatterns, search } = this.props; this.setState({ error: undefined, @@ -388,7 +393,10 @@ export class Flyout extends Component { resolutions, conflictedIndexPatterns!, isOverwriteAllChecked, - indexPatterns + { + indexPatterns, + search, + } ); } this.setState({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index ddb262138d565..d9e39f31d181a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -32,7 +32,7 @@ import { EuiText, EuiSpacer, } from '@elastic/eui'; -import { FilterConfig } from '@elastic/eui/src/components/search_bar/filters/filters'; +import { SearchFilterConfig } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { IBasePath } from 'src/core/public'; @@ -284,7 +284,7 @@ export class Relationships extends Component { let overlays: ReturnType; let notifications: ReturnType; let savedObjects: ReturnType; + let search: ReturnType['search']; const shallowRender = (overrides: Partial = {}) => { return (shallowWithI18nProvider( @@ -106,6 +107,7 @@ describe('SavedObjectsTable', () => { overlays = overlayServiceMock.createStartContract(); notifications = notificationServiceMock.createStartContract(); savedObjects = savedObjectsServiceMock.createStartContract(); + search = dataPluginMock.createStartContract().search; const applications = applicationServiceMock.createStartContract(); applications.capabilities = { @@ -141,6 +143,7 @@ describe('SavedObjectsTable', () => { perPageConfig: 15, goInspectObject: () => {}, canGoInApp: () => true, + search, }; findObjectsMock.mockImplementation(() => ({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index c76fea5a0fb29..b9ebaf2b236f4 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -73,6 +73,7 @@ import { SavedObjectsManagementActionServiceStart, } from '../../services'; import { Header, Table, Flyout, Relationships } from './components'; +import { DataPublicPluginStart } from '../../../../../plugins/data/public'; interface ExportAllOption { id: string; @@ -86,6 +87,7 @@ export interface SavedObjectsTableProps { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; http: HttpStart; + search: DataPublicPluginStart['search']; overlays: OverlayStart; notifications: NotificationsStart; applications: ApplicationStart; @@ -467,6 +469,7 @@ export class SavedObjectsTable extends Component ); } diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index c8dede3da9263..28eac96dcbf46 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -116,8 +116,9 @@ export class SavedObjectsManagementPlugin }; } - public start(core: CoreStart) { + public start(core: CoreStart, { data }: StartDependencies) { const actionStart = this.actionService.start(); + return { actions: actionStart, }; diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts index 4e0f02242961a..fd88f520205f5 100644 --- a/src/plugins/telemetry/public/mocks.ts +++ b/src/plugins/telemetry/public/mocks.ts @@ -25,11 +25,20 @@ import { httpServiceMock } from '../../../core/public/http/http_service.mock'; import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock'; import { TelemetryService } from './services/telemetry_service'; import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications'; -import { TelemetryPluginStart } from './plugin'; +import { TelemetryPluginStart, TelemetryPluginConfig } from './plugin'; + +// The following is to be able to access private methods +/* eslint-disable dot-notation */ + +export interface TelemetryServiceMockOptions { + reportOptInStatusChange?: boolean; + config?: Partial; +} export function mockTelemetryService({ reportOptInStatusChange, -}: { reportOptInStatusChange?: boolean } = {}) { + config: configOverride = {}, +}: TelemetryServiceMockOptions = {}) { const config = { enabled: true, url: 'http://localhost', @@ -39,14 +48,22 @@ export function mockTelemetryService({ banner: true, allowChangingOptInStatus: true, telemetryNotifyUserAboutOptInDefault: true, + ...configOverride, }; - return new TelemetryService({ + const telemetryService = new TelemetryService({ config, http: httpServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), reportOptInStatusChange, }); + + const originalReportOptInStatus = telemetryService['reportOptInStatus']; + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(optInPayload => { + return originalReportOptInStatus(optInPayload); // Actually calling the original method + }); + + return telemetryService; } export function mockTelemetryNotifications({ diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index 0a49b0ae3084e..16faa0cfc7536 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -67,17 +67,31 @@ describe('TelemetryService', () => { }); describe('setOptIn', () => { + it('does not call the api if canChangeOptInStatus==false', async () => { + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: false }, + }); + expect(await telemetryService.setOptIn(true)).toBe(false); + + expect(telemetryService['http'].post).toBeCalledTimes(0); + }); + it('calls api if canChangeOptInStatus', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['http'].post).toBeCalledTimes(1); }); it('sends enabled true if optedIn: true', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); const optedIn = true; await telemetryService.setOptIn(optedIn); @@ -87,8 +101,10 @@ describe('TelemetryService', () => { }); it('sends enabled false if optedIn: false', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); const optedIn = false; await telemetryService.setOptIn(optedIn); @@ -98,9 +114,10 @@ describe('TelemetryService', () => { }); it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); @@ -108,9 +125,10 @@ describe('TelemetryService', () => { }); it('calls reportOptInStatus if reportOptInStatusChange is true', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: true, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); @@ -118,9 +136,10 @@ describe('TelemetryService', () => { }); it('adds an error toast on api error', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { if (url === '/api/telemetry/v2/optIn') { throw Error('failed to update opt in.'); @@ -133,9 +152,13 @@ describe('TelemetryService', () => { expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); }); + // This one should not happen because the entire method is fully caught but hey! :) it('adds an error toast on reportOptInStatus error', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: true, + config: { allowChangingOptInStatus: true }, + }); + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => { throw Error('failed to report OptIn Status.'); }); @@ -146,4 +169,50 @@ describe('TelemetryService', () => { expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); }); }); + + describe('getTelemetryUrl', () => { + it('should return the config.url parameter', async () => { + const url = 'http://test.com'; + const telemetryService = mockTelemetryService({ + config: { url }, + }); + + expect(telemetryService.getTelemetryUrl()).toBe(url); + }); + }); + + describe('setUserHasSeenNotice', () => { + it('should hit the API and change the config', async () => { + const telemetryService = mockTelemetryService({ + config: { telemetryNotifyUserAboutOptInDefault: undefined }, + }); + + expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + await telemetryService.setUserHasSeenNotice(); + expect(telemetryService['http'].put).toBeCalledTimes(1); + expect(telemetryService.userHasSeenOptedInNotice).toBe(true); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(true); + }); + + it('should show a toast notification if the request fail', async () => { + const telemetryService = mockTelemetryService({ + config: { telemetryNotifyUserAboutOptInDefault: undefined }, + }); + + telemetryService['http'].put = jest.fn().mockImplementation((url: string) => { + if (url === '/api/telemetry/v2/userHasSeenNotice') { + throw Error('failed to update opt in.'); + } + }); + + expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + await telemetryService.setUserHasSeenNotice(); + expect(telemetryService['http'].put).toBeCalledTimes(1); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + expect(telemetryService.userHasSeenOptedInNotice).toBe(false); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + }); + }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index cac4e3fdf5f50..6d87a74197fe5 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -122,11 +122,15 @@ export class TelemetryService { } try { - await this.http.post('/api/telemetry/v2/optIn', { + // Report the option to the Kibana server to store the settings. + // It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}] + const optInPayload = await this.http.post('/api/telemetry/v2/optIn', { body: JSON.stringify({ enabled: optedIn }), }); if (this.reportOptInStatusChange) { - await this.reportOptInStatus(optedIn); + // Use the response to report about the change to the remote telemetry cluster. + // If it's opt-out, this will be the last communication to the remote service. + await this.reportOptInStatus(optInPayload); } this.isOptedIn = optedIn; } catch (err) { @@ -162,7 +166,11 @@ export class TelemetryService { } }; - private reportOptInStatus = async (OptInStatus: boolean): Promise => { + /** + * Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service + * @param optInPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings + */ + private reportOptInStatus = async (optInPayload: string[]): Promise => { const telemetryOptInStatusUrl = this.getOptInStatusUrl(); try { @@ -171,7 +179,7 @@ export class TelemetryService { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ enabled: OptInStatus }), + body: JSON.stringify(optInPayload), }); } catch (err) { // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index e65ade0ab8aaa..4ed5dbf251275 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -22,7 +22,10 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; +import { + StatsGetterConfig, + TelemetryCollectionManagerPluginSetup, +} from 'src/plugins/telemetry_collection_manager/server'; import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config'; import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats'; @@ -79,23 +82,30 @@ export function registerTelemetryOptInRoutes({ }); } + const statsGetterConfig: StatsGetterConfig = { + start: moment() + .subtract(20, 'minutes') + .toISOString(), + end: moment().toISOString(), + unencrypted: false, + }; + + const optInStatus = await telemetryCollectionManager.getOptInStats( + newOptInStatus, + statsGetterConfig + ); + if (config.sendUsageFrom === 'server') { const optInStatusUrl = config.optInStatusUrl; await sendTelemetryOptInStatus( telemetryCollectionManager, { optInStatusUrl, newOptInStatus }, - { - start: moment() - .subtract(20, 'minutes') - .toISOString(), - end: moment().toISOString(), - unencrypted: false, - } + statsGetterConfig ); } await updateTelemetrySavedObject(context.core.savedObjects.client, attributes); - return res.ok({}); + return res.ok({ body: optInStatus }); } ); } diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index c7e6d61e15f31..e6247a8bafff7 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -19,9 +19,10 @@ import { ActionByType } from './actions/action'; import { TriggerInternal } from './triggers/trigger_internal'; -import { EmbeddableVisTriggerContext, IEmbeddable } from '../../embeddable/public'; import { Filter } from '../../data/public'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; +import { IEmbeddable } from '../../embeddable/public'; +import { RangeSelectTriggerContext, ValueClickTriggerContext } from '../../embeddable/public'; export type TriggerRegistry = Map>; export type ActionRegistry = Map>; @@ -36,8 +37,8 @@ export type TriggerContext = BaseContext; export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; - [SELECT_RANGE_TRIGGER]: EmbeddableVisTriggerContext; - [VALUE_CLICK_TRIGGER]: EmbeddableVisTriggerContext; + [SELECT_RANGE_TRIGGER]: RangeSelectTriggerContext; + [VALUE_CLICK_TRIGGER]: ValueClickTriggerContext; [APPLY_FILTER_TRIGGER]: { embeddable: IEmbeddable; filters: Filter[]; diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index ecbc41f28003c..72515d0845926 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -170,7 +170,7 @@ function DefaultEditorAggGroup({ agg={agg} aggIndex={index} aggIsTooLow={calcAggIsTooLow(agg, index, group, schemas)} - dragHandleProps={provided.dragHandleProps} + dragHandleProps={provided.dragHandleProps || null} formIsTouched={aggsState[agg.id] ? aggsState[agg.id].touched : false} groupName={groupName} isDraggable={stats.count > 1} diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index 5af3cfc5b0928..9bc3146b9903b 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -58,6 +58,8 @@ const buckets = { size: controls.SizeParamEditor, }, [BUCKET_TYPES.TERMS]: { + include: controls.IncludeExcludeParamEditor, + exclude: controls.IncludeExcludeParamEditor, orderBy: controls.OrderByParamEditor, orderAgg: controls.OrderAggParamEditor, order: wrapWithInlineComp(controls.OrderParamEditor), diff --git a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts index dac86249ebbb9..5cb594ade8dba 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts @@ -119,7 +119,7 @@ function getNextModel(list: NumberRowModel[], range: NumberListRange): NumberRow }; } -function getInitModelList(list: Array): NumberRowModel[] { +function getInitModelList(list: Array): NumberRowModel[] { return list.length ? list.map(num => ({ value: (num === undefined ? EMPTY_STRING : num) as NumberRowModel['value'], diff --git a/src/plugins/vis_default_editor/public/components/controls/components/simple_number_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/simple_number_list.tsx new file mode 100644 index 0000000000000..becf8e47ef573 --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/components/simple_number_list.tsx @@ -0,0 +1,140 @@ +/* + * 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. + */ + +import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { isArray } from 'lodash'; +import { EuiButtonEmpty, EuiFlexItem, EuiFormRow, EuiSpacer, htmlIdGenerator } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EMPTY_STRING, getInitModelList, getRange, parse } from './number_list/utils'; +import { NumberRow, NumberRowModel } from './number_list/number_row'; +import { AggParamEditorProps } from '../../agg_param_props'; + +const generateId = htmlIdGenerator(); + +function SimpleNumberList({ + agg, + aggParam, + value, + setValue, + setTouched, +}: AggParamEditorProps>) { + const [numbers, setNumbers] = useState( + getInitModelList(value && isArray(value) ? value : [EMPTY_STRING]) + ); + const numberRange = useMemo(() => getRange('[-Infinity,Infinity]'), []); + + // This useEffect is needed to discard changes, it sets numbers a mapped value if they are different + useEffect(() => { + if ( + isArray(value) && + (value.length !== numbers.length || + !value.every((numberValue, index) => numberValue === numbers[index].value)) + ) { + setNumbers( + value.map(numberValue => ({ + id: generateId(), + value: numberValue, + isInvalid: false, + })) + ); + } + }, [numbers, value]); + + const onUpdate = useCallback( + (numberList: NumberRowModel[]) => { + setNumbers(numberList); + setValue(numberList.map(({ value: numberValue }) => numberValue)); + }, + [setValue] + ); + + const onChangeValue = useCallback( + (numberField: { id: string; value: string }) => { + onUpdate( + numbers.map(number => + number.id === numberField.id + ? { + id: numberField.id, + value: parse(numberField.value), + isInvalid: false, + } + : number + ) + ); + }, + [numbers, onUpdate] + ); + + // Add an item to the end of the list + const onAdd = useCallback(() => { + const newArray = [ + ...numbers, + { + id: generateId(), + value: EMPTY_STRING as '', + isInvalid: false, + }, + ]; + onUpdate(newArray); + }, [numbers, onUpdate]); + + const onDelete = useCallback( + (id: string) => onUpdate(numbers.filter(number => number.id !== id)), + [numbers, onUpdate] + ); + + return ( + + <> + {numbers.map((number, arrayIndex) => ( + + + {numbers.length - 1 !== arrayIndex && } + + ))} + + + + + + + + + ); +} + +export { SimpleNumberList }; diff --git a/src/plugins/vis_default_editor/public/components/controls/include_exclude.tsx b/src/plugins/vis_default_editor/public/components/controls/include_exclude.tsx new file mode 100644 index 0000000000000..f60f6ce7ce249 --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/include_exclude.tsx @@ -0,0 +1,49 @@ +/* + * 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. + */ + +import React, { useEffect } from 'react'; +import { AggParamEditorProps } from '../agg_param_props'; +import { StringParamEditor } from './string'; +import { search } from '../../../../data/public'; +import { SimpleNumberList } from './components/simple_number_list'; +const { isNumberType } = search.aggs; + +export function IncludeExcludeParamEditor(props: AggParamEditorProps>) { + const { agg, value, setValue } = props; + const isAggOfNumberType = isNumberType(agg); + + // This useEffect converts value from string type to number and back when the field type is changed + useEffect(() => { + if (isAggOfNumberType && !Array.isArray(value) && value !== undefined) { + const numberArray = value + .split('|') + .map(item => parseFloat(item)) + .filter(number => Number.isFinite(number)); + setValue(numberArray.length ? numberArray : ['']); + } else if (!isAggOfNumberType && Array.isArray(value) && value !== undefined) { + setValue(value.filter(item => item !== '').join('|')); + } + }, [isAggOfNumberType, setValue, value]); + + return isAggOfNumberType ? ( + } /> + ) : ( + + ); +} diff --git a/src/plugins/vis_default_editor/public/components/controls/index.ts b/src/plugins/vis_default_editor/public/components/controls/index.ts index e8944aa667853..cfb236e5e22e3 100644 --- a/src/plugins/vis_default_editor/public/components/controls/index.ts +++ b/src/plugins/vis_default_editor/public/components/controls/index.ts @@ -24,6 +24,7 @@ export { ExtendedBoundsParamEditor } from './extended_bounds'; export { FieldParamEditor } from './field'; export { FiltersParamEditor } from './filters'; export { HasExtendedBoundsParamEditor } from './has_extended_bounds'; +export { IncludeExcludeParamEditor } from './include_exclude'; export { IpRangesParamEditor } from './ip_ranges'; export { IpRangeTypeParamEditor } from './ip_range_type'; export { IsFilteredByCollarParamEditor } from './is_filtered_by_collar'; diff --git a/src/plugins/vis_type_table/config.ts b/src/plugins/vis_type_table/config.ts new file mode 100644 index 0000000000000..6749bd83de39f --- /dev/null +++ b/src/plugins/vis_type_table/config.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_table/kibana.json b/src/plugins/vis_type_table/kibana.json new file mode 100644 index 0000000000000..bb0f6478a4240 --- /dev/null +++ b/src/plugins/vis_type_table/kibana.json @@ -0,0 +1,11 @@ +{ + "id": "visTypeTable", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "expressions", + "visualizations", + "data" + ] +} diff --git a/src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap rename to src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_table/public/_table_vis.scss b/src/plugins/vis_type_table/public/_table_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/_table_vis.scss rename to src/plugins/vis_type_table/public/_table_vis.scss diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/_agg_table.scss rename to src/plugins/vis_type_table/public/agg_table/_agg_table.scss diff --git a/src/plugins/vis_type_table/public/agg_table/_index.scss b/src/plugins/vis_type_table/public/agg_table/_index.scss new file mode 100644 index 0000000000000..340e08a76f1bd --- /dev/null +++ b/src/plugins/vis_type_table/public/agg_table/_index.scss @@ -0,0 +1 @@ +@import './agg_table'; diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.html b/src/plugins/vis_type_table/public/agg_table/agg_table.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.html rename to src/plugins/vis_type_table/public/agg_table/agg_table.html diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js new file mode 100644 index 0000000000000..0cd501e2d0344 --- /dev/null +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.js @@ -0,0 +1,272 @@ +/* + * 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. + */ +import _ from 'lodash'; +import aggTableTemplate from './agg_table.html'; +import { getFormatService } from '../services'; +import { i18n } from '@kbn/i18n'; + +export function KbnAggTable(config, RecursionHelper) { + return { + restrict: 'E', + template: aggTableTemplate, + scope: { + table: '=', + dimensions: '=', + perPage: '=?', + sort: '=?', + exportTitle: '=?', + showTotal: '=', + totalFunc: '=', + percentageCol: '=', + filter: '=', + }, + controllerAs: 'aggTable', + compile: function($el) { + // Use the compile function from the RecursionHelper, + // And return the linking function(s) which it returns + return RecursionHelper.compile($el); + }, + controller: function($scope) { + const self = this; + + self._saveAs = require('@elastic/filesaver').saveAs; + self.csv = { + separator: config.get('csv:separator'), + quoteValues: config.get('csv:quoteValues'), + }; + + self.exportAsCsv = function(formatted) { + const csv = new Blob([self.toCsv(formatted)], { type: 'text/plain;charset=utf-8' }); + self._saveAs(csv, self.csv.filename); + }; + + self.toCsv = function(formatted) { + const rows = $scope.table.rows; + const columns = formatted ? $scope.formattedColumns : $scope.table.columns; + const nonAlphaNumRE = /[^a-zA-Z0-9]/; + const allDoubleQuoteRE = /"/g; + + function escape(val) { + if (!formatted && _.isObject(val)) val = val.valueOf(); + val = String(val); + if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { + val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; + } + return val; + } + + // escape each cell in each row + const csvRows = rows.map(function(row) { + return Object.entries(row).map(([k, v]) => { + const column = columns.find(c => c.id === k); + if (formatted && column) { + return escape(column.formatter.convert(v)); + } + return escape(v); + }); + }); + + // add the columns to the rows + csvRows.unshift( + columns.map(function(col) { + return escape(formatted ? col.title : col.name); + }) + ); + + return csvRows + .map(function(row) { + return row.join(self.csv.separator) + '\r\n'; + }) + .join(''); + }; + + $scope.$watchMulti( + ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], + function() { + const { table, exportTitle, percentageCol } = $scope; + const showPercentage = percentageCol !== ''; + + if (!table) { + $scope.rows = null; + $scope.formattedColumns = null; + return; + } + + self.csv.filename = (exportTitle || table.title || 'table') + '.csv'; + $scope.rows = table.rows; + $scope.formattedColumns = []; + + if (typeof $scope.dimensions === 'undefined') return; + + const { buckets, metrics, splitColumn } = $scope.dimensions; + + $scope.formattedColumns = table.columns + .map(function(col, i) { + const isBucket = buckets.find(bucket => bucket.accessor === i); + const isSplitColumn = splitColumn + ? splitColumn.find(splitColumn => splitColumn.accessor === i) + : undefined; + const dimension = + isBucket || isSplitColumn || metrics.find(metric => metric.accessor === i); + + if (!dimension) return; + + const formatter = getFormatService().deserialize(dimension.format); + + const formattedColumn = { + id: col.id, + title: col.name, + formatter: formatter, + filterable: !!isBucket, + }; + + const last = i === table.columns.length - 1; + + if (last || !isBucket) { + formattedColumn.class = 'visualize-table-right'; + } + + const isDate = + dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + + let { totalFunc } = $scope; + if (typeof totalFunc === 'undefined' && showPercentage) { + totalFunc = 'sum'; + } + + if (allowsNumericalAggregations || isDate || totalFunc === 'count') { + const sum = tableRows => { + return _.reduce( + tableRows, + function(prev, curr) { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + curr[col.id]; + }, + 0 + ); + }; + + formattedColumn.sumTotal = sum(table.rows); + switch (totalFunc) { + case 'sum': { + if (!isDate) { + const total = formattedColumn.sumTotal; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = formattedColumn.sumTotal; + } + break; + } + case 'avg': { + if (!isDate) { + const total = sum(table.rows) / table.rows.length; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + } + break; + } + case 'min': { + const total = _.chain(table.rows) + .map(col.id) + .min() + .value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'max': { + const total = _.chain(table.rows) + .map(col.id) + .max() + .value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'count': { + const total = table.rows.length; + formattedColumn.formattedTotal = total; + formattedColumn.total = total; + break; + } + default: + break; + } + } + + return formattedColumn; + }) + .filter(column => column); + + if (showPercentage) { + const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); + + // column to show percentage for was removed + if (insertAtIndex < 0) return; + + const { cols, rows } = addPercentageCol( + $scope.formattedColumns, + percentageCol, + table.rows, + insertAtIndex + ); + $scope.rows = rows; + $scope.formattedColumns = cols; + } + } + ); + }, + }; +} + +/** + * @param {Object[]} columns - the formatted columns that will be displayed + * @param {String} title - the title of the column to add to + * @param {Object[]} rows - the row data for the columns + * @param {Number} insertAtIndex - the index to insert the percentage column at + * @returns {Object} - cols and rows for the table to render now included percentage column(s) + */ +function addPercentageCol(columns, title, rows, insertAtIndex) { + const { id, sumTotal } = columns[insertAtIndex]; + const newId = `${id}-percents`; + const formatter = getFormatService().deserialize({ id: 'percent' }); + const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { + defaultMessage: '{title} percentages', + values: { title }, + }); + const newCols = insert(columns, insertAtIndex, { + title: i18nTitle, + id: newId, + formatter, + }); + const newRows = rows.map(row => ({ + [newId]: formatter.convert(row[id] / sumTotal / 100), + ...row, + })); + + return { cols: newCols, rows: newRows }; +} + +function insert(arr, index, ...items) { + const newArray = [...arr]; + newArray.splice(index + 1, 0, ...items); + return newArray; +} diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.html b/src/plugins/vis_type_table/public/agg_table/agg_table_group.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.html rename to src/plugins/vis_type_table/public/agg_table/agg_table_group.html diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.js rename to src/plugins/vis_type_table/public/agg_table/agg_table_group.js diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx rename to src/plugins/vis_type_table/public/components/table_vis_options.tsx index 265528f33f9cd..68348d5ef1060 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -24,12 +24,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { search } from '../../../../../plugins/data/public'; -import { - SwitchOption, - SelectOption, - NumberInputOption, -} from '../../../../../plugins/charts/public'; +import { search } from '../../../data/public'; +import { SwitchOption, SelectOption, NumberInputOption } from '../../../charts/public'; import { TableVisParams } from '../types'; import { totalAggregations } from './utils'; diff --git a/src/legacy/core_plugins/vis_type_table/public/components/utils.ts b/src/plugins/vis_type_table/public/components/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/components/utils.ts rename to src/plugins/vis_type_table/public/components/utils.ts diff --git a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/get_inner_angular.ts similarity index 91% rename from src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts rename to src/plugins/vis_type_table/public/get_inner_angular.ts index 6208e358b4184..d69b9bba31b03 100644 --- a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/get_inner_angular.ts @@ -23,7 +23,7 @@ import angular from 'angular'; import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; +import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; import { initAngularBootstrap, PaginateDirectiveProvider, @@ -32,15 +32,15 @@ import { watchMultiDecorator, KbnAccessibleClickProvider, configureAppAngularModule, -} from '../../../../plugins/kibana_legacy/public'; +} from '../../kibana_legacy/public'; initAngularBootstrap(); const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; -export function getAngularModule(name: string, core: CoreStart) { +export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { const uiModule = getInnerAngular(name, core); - configureAppAngularModule(uiModule, core as LegacyCoreStart, true); + configureAppAngularModule(uiModule, { core, env: context.env }, true); return uiModule; } diff --git a/src/plugins/vis_type_table/public/index.scss b/src/plugins/vis_type_table/public/index.scss new file mode 100644 index 0000000000000..0972c85e0dbe0 --- /dev/null +++ b/src/plugins/vis_type_table/public/index.scss @@ -0,0 +1,10 @@ +// Prefix all styles with "tbv" to avoid conflicts. +// Examples +// tbvChart +// tbvChart__legend +// tbvChart__legend--small +// tbvChart__legend-isLoading + +@import './agg_table/index'; +@import './paginated_table/index'; +@import './table_vis'; diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts new file mode 100644 index 0000000000000..5621fdb094772 --- /dev/null +++ b/src/plugins/vis_type_table/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ +import './index.scss'; +import { PluginInitializerContext } from 'kibana/public'; +import { TableVisPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_table/public/paginated_table/_index.scss b/src/plugins/vis_type_table/public/paginated_table/_index.scss new file mode 100644 index 0000000000000..23d56c09b2818 --- /dev/null +++ b/src/plugins/vis_type_table/public/paginated_table/_index.scss @@ -0,0 +1 @@ +@import './_table_cell_filter'; diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss b/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss rename to src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.html b/src/plugins/vis_type_table/public/paginated_table/paginated_table.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.html rename to src/plugins/vis_type_table/public/paginated_table/paginated_table.html diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.js b/src/plugins/vis_type_table/public/paginated_table/paginated_table.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.js rename to src/plugins/vis_type_table/public/paginated_table/paginated_table.js diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts rename to src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts index 7352236f03feb..23e4aee0378dc 100644 --- a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts +++ b/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts @@ -25,10 +25,9 @@ import 'angular-mocks'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { coreMock } from '../../../../../core/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; -jest.mock('ui/new_platform'); -jest.mock('../../../../../plugins/kibana_legacy/public/angular/angular_config', () => ({ +jest.mock('../../../kibana_legacy/public/angular/angular_config', () => ({ configureAppAngularModule: () => {}, })); @@ -73,7 +72,11 @@ describe('Table Vis - Paginated table', () => { let paginatedTable: any; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', coreMock.createStart()); + const tableVisModule = getAngularModule( + 'kibana/table_vis', + coreMock.createStart(), + coreMock.createPluginInitializerContext() + ); initTableVisLegacyModule(tableVisModule); }; diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/paginated_table/rows.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/rows.js rename to src/plugins/vis_type_table/public/paginated_table/rows.js diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/table_cell_filter.html rename to src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts new file mode 100644 index 0000000000000..a41d939523bcc --- /dev/null +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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. + */ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; + +import { createTableVisFn } from './table_vis_fn'; +import { getTableVisTypeDefinition } from './table_vis_type'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService } from './services'; + +/** @internal */ +export interface TablePluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; +} + +/** @internal */ +export interface TablePluginStartDependencies { + data: DataPublicPluginStart; +} + +/** @internal */ +export class TableVisPlugin implements Plugin, void> { + initializerContext: PluginInitializerContext; + createBaseVisualization: any; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public async setup( + core: CoreSetup, + { expressions, visualizations }: TablePluginSetupDependencies + ) { + expressions.registerFunction(createTableVisFn); + visualizations.createBaseVisualization( + getTableVisTypeDefinition(core, this.initializerContext) + ); + } + + public start(core: CoreStart, { data }: TablePluginStartDependencies) { + setFormatService(data.fieldFormats); + } +} diff --git a/src/plugins/vis_type_table/public/services.ts b/src/plugins/vis_type_table/public/services.ts new file mode 100644 index 0000000000000..3aaffe75e27f1 --- /dev/null +++ b/src/plugins/vis_type_table/public/services.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('table data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis.html b/src/plugins/vis_type_table/public/table_vis.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis.html rename to src/plugins/vis_type_table/public/table_vis.html diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.js b/src/plugins/vis_type_table/public/table_vis_controller.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_controller.js rename to src/plugins/vis_type_table/public/table_vis_controller.js diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/table_vis_controller.test.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts rename to src/plugins/vis_type_table/public/table_vis_controller.test.ts index 8d6f88bf8dd4a..4607324ca150c 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_controller.test.ts @@ -26,24 +26,23 @@ import $ from 'jquery'; import StubIndexPattern from 'test_utils/stub_index_pattern'; import { getAngularModule } from './get_inner_angular'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; -import { tableVisTypeDefinition } from './table_vis_type'; -import { Vis } from '../../../../plugins/visualizations/public'; +import { getTableVisTypeDefinition } from './table_vis_type'; +import { Vis } from '../../visualizations/public'; // eslint-disable-next-line -import { stubFields } from '../../../../plugins/data/public/stubs'; +import { stubFields } from '../../data/public/stubs'; // eslint-disable-next-line import { tableVisResponseHandler } from './table_vis_response_handler'; -import { coreMock } from '../../../../core/public/mocks'; +import { coreMock } from '../../../core/public/mocks'; +import { IAggConfig, search } from '../../data/public'; +// TODO: remove linting disable // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { npStart } from './legacy_imports'; -import { IAggConfig, search } from '../../../../plugins/data/public'; +import { searchStartMock } from '../../data/public/search/mocks'; -// should be mocked once get rid of 'ui/new_platform' legacy imports -const { createAggConfigs } = npStart.plugins.data.search.aggs; +const { createAggConfigs } = searchStartMock.aggs; const { tabifyAggResponse } = search; -jest.mock('ui/new_platform'); -jest.mock('../../../../plugins/kibana_legacy/public/angular/angular_config', () => ({ +jest.mock('../../kibana_legacy/public/angular/angular_config', () => ({ configureAppAngularModule: () => {}, })); @@ -89,7 +88,11 @@ describe('Table Vis - Controller', () => { let stubIndexPattern: any; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', coreMock.createStart()); + const tableVisModule = getAngularModule( + 'kibana/table_vis', + coreMock.createStart(), + coreMock.createPluginInitializerContext() + ); initTableVisLegacyModule(tableVisModule); }; @@ -110,9 +113,13 @@ describe('Table Vis - Controller', () => { (cfg: any) => cfg, 'time', stubFields, - coreMock.createStart() + coreMock.createSetup() ); }); + const tableVisTypeDefinition = getTableVisTypeDefinition( + coreMock.createSetup(), + coreMock.createPluginInitializerContext() + ); function getRangeVis(params?: object) { return ({ diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts b/src/plugins/vis_type_table/public/table_vis_fn.test.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts rename to src/plugins/vis_type_table/public/table_vis_fn.test.ts index 36392c10f93f3..9accf8950d910 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.test.ts @@ -21,7 +21,7 @@ import { createTableVisFn } from './table_vis_fn'; import { tableVisResponseHandler } from './table_vis_response_handler'; // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; jest.mock('./table_vis_response_handler', () => ({ tableVisResponseHandler: jest.fn().mockReturnValue({ diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts rename to src/plugins/vis_type_table/public/table_vis_fn.ts index a97e596e89754..9739a7a284e6c 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; export type Input = KibanaDatatable; diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_legacy_module.ts b/src/plugins/vis_type_table/public/table_vis_legacy_module.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_legacy_module.ts rename to src/plugins/vis_type_table/public/table_vis_legacy_module.ts diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/table_vis_response_handler.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts rename to src/plugins/vis_type_table/public/table_vis_response_handler.ts diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts new file mode 100644 index 0000000000000..26e5ac8cfd71a --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -0,0 +1,100 @@ +/* + * 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. + */ +import { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; +import { Vis } from '../../visualizations/public'; +import { tableVisResponseHandler } from './table_vis_response_handler'; +// @ts-ignore +import tableVisTemplate from './table_vis.html'; +import { TableOptions } from './components/table_vis_options'; +import { getTableVisualizationControllerClass } from './vis_controller'; + +export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { + return { + type: 'table', + name: 'table', + title: i18n.translate('visTypeTable.tableVisTitle', { + defaultMessage: 'Data Table', + }), + icon: 'visTable', + description: i18n.translate('visTypeTable.tableVisDescription', { + defaultMessage: 'Display values in a table', + }), + visualization: getTableVisualizationControllerClass(core, context), + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + template: tableVisTemplate, + }, + editorConfig: { + optionsTemplate: TableOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + min: 1, + defaults: [{ type: 'count', schema: 'metric' }], + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + }, + ]), + }, + responseHandler: tableVisResponseHandler, + hierarchicalData: (vis: Vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + }, + }; +} diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts new file mode 100644 index 0000000000000..39023d1305cb6 --- /dev/null +++ b/src/plugins/vis_type_table/public/types.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +import { SchemaConfig } from '../../visualizations/public'; + +export enum AggTypes { + SUM = 'sum', + AVG = 'avg', + MIN = 'min', + MAX = 'max', + COUNT = 'count', +} + +export interface Dimensions { + buckets: SchemaConfig[]; + metrics: SchemaConfig[]; +} + +export interface TableVisParams { + type: 'table'; + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; + sort: { + columnIndex: number | null; + direction: string | null; + }; + showTotal: boolean; + totalFunc: AggTypes; + percentageCol: string; + dimensions: Dimensions; +} diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/vis_controller.ts new file mode 100644 index 0000000000000..d49dd32c8c89c --- /dev/null +++ b/src/plugins/vis_type_table/public/vis_controller.ts @@ -0,0 +1,109 @@ +/* + * 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. + */ +import { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; +import $ from 'jquery'; + +import { VisParams, ExprVis } from '../../visualizations/public'; +import { getAngularModule } from './get_inner_angular'; +import { initTableVisLegacyModule } from './table_vis_legacy_module'; + +const innerAngularName = 'kibana/table_vis'; + +export function getTableVisualizationControllerClass( + core: CoreSetup, + context: PluginInitializerContext +) { + return class TableVisualizationController { + private tableVisModule: IModule | undefined; + private injector: auto.IInjectorService | undefined; + el: JQuery; + vis: ExprVis; + $rootScope: IRootScopeService | null = null; + $scope: (IScope & { [key: string]: any }) | undefined; + $compile: ICompileService | undefined; + + constructor(domeElement: Element, vis: ExprVis) { + this.el = $(domeElement); + this.vis = vis; + } + + getInjector() { + if (!this.injector) { + const mountpoint = document.createElement('div'); + mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); + this.injector = angular.bootstrap(mountpoint, [innerAngularName]); + this.el.append(mountpoint); + } + + return this.injector; + } + + async initLocalAngular() { + if (!this.tableVisModule) { + const [coreStart] = await core.getStartServices(); + this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); + initTableVisLegacyModule(this.tableVisModule); + } + } + + async render(esResponse: object, visParams: VisParams) { + await this.initLocalAngular(); + + return new Promise(async (resolve, reject) => { + if (!this.$rootScope) { + const $injector = this.getInjector(); + this.$rootScope = $injector.get('$rootScope'); + this.$compile = $injector.get('$compile'); + } + const updateScope = () => { + if (!this.$scope) { + return; + } + this.$scope.vis = this.vis; + this.$scope.visState = { params: visParams }; + this.$scope.esResponse = esResponse; + + this.$scope.visParams = visParams; + this.$scope.renderComplete = resolve; + this.$scope.renderFailed = reject; + this.$scope.resize = Date.now(); + this.$scope.$apply(); + }; + + if (!this.$scope && this.$compile) { + this.$scope = this.$rootScope.$new(); + this.$scope.uiState = this.vis.getUiState(); + updateScope(); + this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); + this.$scope.$apply(); + } else { + updateScope(); + } + }); + } + + destroy() { + if (this.$rootScope) { + this.$rootScope.$destroy(); + this.$rootScope = null; + } + } + }; +} diff --git a/src/plugins/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts new file mode 100644 index 0000000000000..882958a28777d --- /dev/null +++ b/src/plugins/vis_type_table/server/index.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/vis_type_tagcloud/config.ts b/src/plugins/vis_type_tagcloud/config.ts new file mode 100644 index 0000000000000..6749bd83de39f --- /dev/null +++ b/src/plugins/vis_type_tagcloud/config.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_type_tagcloud/kibana.json new file mode 100644 index 0000000000000..dbc9a1b9ef692 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "visTypeTagcloud", + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["data", "expressions", "visualizations", "charts"] +} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap rename to src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/_tag_cloud.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss rename to src/plugins/vis_type_tagcloud/public/_tag_cloud.scss diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js rename to src/plugins/vis_type_tagcloud/public/components/feedback_message.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js rename to src/plugins/vis_type_tagcloud/public/components/label.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx similarity index 91% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index 7a64549edd747..d33576e4e5529 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,9 +20,9 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; -import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public'; +import { VisOptionsProps } from '../../../vis_default_editor/public'; +import { ValidatedDualRange } from '../../../kibana_react/public'; +import { SelectOption, SwitchOption } from '../../../charts/public'; import { TagCloudVisParams } from '../types'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js diff --git a/src/plugins/vis_type_tagcloud/public/index.scss b/src/plugins/vis_type_tagcloud/public/index.scss new file mode 100644 index 0000000000000..e6893b9a2474c --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/index.scss @@ -0,0 +1,8 @@ +// Prefix all styles with "tgc" to avoid conflicts. +// Examples +// tgcChart +// tgcChart__legend +// tgcChart__legend--small +// tgcChart__legend-isLoading + +@import './tag_cloud'; diff --git a/src/plugins/vis_type_tagcloud/public/index.ts b/src/plugins/vis_type_tagcloud/public/index.ts new file mode 100644 index 0000000000000..ff27d96b710fa --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { TagCloudPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_tagcloud/public/plugin.ts b/src/plugins/vis_type_tagcloud/public/plugin.ts new file mode 100644 index 0000000000000..6978186058b1d --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/plugin.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +import { createTagCloudFn } from './tag_cloud_fn'; +import { createTagCloudVisTypeDefinition } from './tag_cloud_type'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService } from './services'; +import { ConfigSchema } from '../config'; + +import './index.scss'; + +/** @internal */ +export interface TagCloudPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface TagCloudVisDependencies { + colors: ChartsPluginSetup['colors']; +} + +/** @internal */ +export interface TagCloudVisPluginStartDependencies { + data: DataPublicPluginStart; +} + +/** @internal */ +export class TagCloudPlugin implements Plugin { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup( + core: CoreSetup, + { expressions, visualizations, charts }: TagCloudPluginSetupDependencies + ) { + const visualizationDependencies: TagCloudVisDependencies = { + colors: charts.colors, + }; + expressions.registerFunction(createTagCloudFn); + visualizations.createBaseVisualization( + createTagCloudVisTypeDefinition(visualizationDependencies) + ); + } + + public start(core: CoreStart, { data }: TagCloudVisPluginStartDependencies) { + setFormatService(data.fieldFormats); + } +} diff --git a/src/plugins/vis_type_tagcloud/public/services.ts b/src/plugins/vis_type_tagcloud/public/services.ts new file mode 100644 index 0000000000000..f6002afc66493 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/services.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts similarity index 91% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts index 65c54766133d1..eb16b0855a138 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts @@ -19,8 +19,7 @@ import { createTagCloudFn } from './tag_cloud_fn'; -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#tagcloud', () => { const fn = functionWrapper(createTagCloudFn()); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts index 31c7fd118cefd..05cf05ab00b75 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; import { TagCloudVisParams } from './types'; const name = 'tagcloud'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index b7dfa62c93fb9..5a8cc3004a315 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { Schemas } from '../../vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/types.ts b/src/plugins/vis_type_tagcloud/public/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/types.ts rename to src/plugins/vis_type_tagcloud/public/types.ts diff --git a/src/plugins/vis_type_tagcloud/server/index.ts b/src/plugins/vis_type_tagcloud/server/index.ts new file mode 100644 index 0000000000000..bd9656b29c524 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/server/index.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap index d269f61beefab..dd454231a9ac2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap @@ -85,6 +85,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_sele "asPlainText": true, } } + sortMatchesBy="none" /> `; diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 78436720dd66c..3bf8b77621cf5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -223,6 +223,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js "asPlainText": true, } } + sortMatchesBy="none" /> diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/index.ts b/src/plugins/vis_type_timeseries/server/saved_objects/index.ts new file mode 100644 index 0000000000000..5f7f5767f423d --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/saved_objects/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { tsvbTelemetrySavedObjectType } from './tsvb_telemetry'; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts similarity index 100% rename from src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts rename to src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index 779d9441df2fd..e4b8ca19094e4 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -19,7 +19,7 @@ import { APICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../usage_collection/server'; -import { tsvbTelemetrySavedObjectType } from './saved_object_type'; +import { tsvbTelemetrySavedObjectType } from '../saved_objects'; export interface ValidationTelemetryServiceSetup { logFailedValidation: () => void; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index 6bfd6c9536df4..f1f82e7f5b7ad 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -2,5 +2,6 @@ "id": "visTypeVega", "version": "kibana", "server": true, - "ui": true + "ui": true, + "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"] } diff --git a/src/plugins/vis_type_vega/public/__mocks__/services.ts b/src/plugins/vis_type_vega/public/__mocks__/services.ts new file mode 100644 index 0000000000000..1bf051232e4c9 --- /dev/null +++ b/src/plugins/vis_type_vega/public/__mocks__/services.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +import { createGetterSetter } from '../../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; + +export const [getData, setData] = createGetterSetter('Data'); +setData(dataPluginMock.createStartContract()); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); +setNotifications(coreMock.createStart().notifications); + +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); +setUISettings(coreMock.createStart().uiSettings); + +export const [getSavedObjects, setSavedObjects] = createGetterSetter( + 'SavedObjects' +); +setSavedObjects(coreMock.createStart().savedObjects); + +export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ + esShardTimeout: number; + enableExternalUrls: boolean; + emsTileLayerId: unknown; +}>('InjectedVars'); +setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + esShardTimeout: 10000, +}); + +export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; +export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; +export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss b/src/plugins/vis_type_vega/public/_vega_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss rename to src/plugins/vis_type_vega/public/_vega_editor.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss rename to src/plugins/vis_type_vega/public/_vega_vis.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/index.ts b/src/plugins/vis_type_vega/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/index.ts rename to src/plugins/vis_type_vega/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_help_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx rename to src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js rename to src/plugins/vis_type_vega/public/data_model/ems_file_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js b/src/plugins/vis_type_vega/public/data_model/search_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js b/src/plugins/vis_type_vega/public/data_model/search_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js b/src/plugins/vis_type_vega/public/data_model/time_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js b/src/plugins/vis_type_vega/public/data_model/time_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js b/src/plugins/vis_type_vega/public/data_model/url_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js rename to src/plugins/vis_type_vega/public/data_model/url_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js b/src/plugins/vis_type_vega/public/data_model/utils.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js rename to src/plugins/vis_type_vega/public/data_model/utils.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson rename to src/plugins/vis_type_vega/public/default.spec.hjson diff --git a/src/plugins/vis_type_vega/public/index.scss b/src/plugins/vis_type_vega/public/index.scss new file mode 100644 index 0000000000000..78d9eb61999f7 --- /dev/null +++ b/src/plugins/vis_type_vega/public/index.scss @@ -0,0 +1,9 @@ +// Prefix all styles with "vga" to avoid conflicts. +// Examples +// vgaChart +// vgaChart__legend +// vgaChart__legend--small +// vgaChart__legend-isLoading + +@import './vega_vis'; +@import './vega_editor'; diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_type_vega/public/index.ts index 71f3474f8217e..78878d38e1674 100644 --- a/src/plugins/vis_type_vega/public/index.ts +++ b/src/plugins/vis_type_vega/public/index.ts @@ -19,20 +19,8 @@ import { PluginInitializerContext } from 'kibana/public'; import { ConfigSchema } from '../config'; +import { VegaPlugin as Plugin } from './plugin'; -export const plugin = (initializerContext: PluginInitializerContext) => ({ - setup() { - return { - /** - * The configuration is temporarily exposed to allow the legacy vega plugin to consume - * the setting. Once the vega plugin is migrated completely, this will become an implementation - * detail. - * @deprecated - */ - config: initializerContext.config.get(), - }; - }, - start() {}, -}); - -export type VisTypeVegaSetup = ReturnType['setup']>; +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts new file mode 100644 index 0000000000000..c312705194cde --- /dev/null +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -0,0 +1,97 @@ +/* + * 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. + */ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { Plugin as DataPublicPlugin } from '../../data/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { + setNotifications, + setData, + setSavedObjects, + setInjectedVars, + setUISettings, +} from './services'; + +import { createVegaFn } from './vega_fn'; +import { createVegaTypeDefinition } from './vega_type'; +import { IServiceSettings } from '../../maps_legacy/public'; +import { ConfigSchema } from '../config'; + +import './index.scss'; + +/** @internal */ +export interface VegaVisualizationDependencies { + core: CoreSetup; + plugins: { + data: ReturnType; + }; + serviceSettings: IServiceSettings; +} + +/** @internal */ +export interface VegaPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + data: ReturnType; + mapsLegacy: any; +} + +/** @internal */ +export interface VegaPluginStartDependencies { + data: ReturnType; +} + +/** @internal */ +export class VegaPlugin implements Plugin, void> { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public async setup( + core: CoreSetup, + { data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies + ) { + setInjectedVars({ + enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, + esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, + emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), + }); + setUISettings(core.uiSettings); + + const visualizationDependencies: Readonly = { + core, + plugins: { + data, + }, + serviceSettings: mapsLegacy.serviceSettings, + }; + + expressions.registerFunction(() => createVegaFn(visualizationDependencies)); + + visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); + } + + public start(core: CoreStart, { data }: VegaPluginStartDependencies) { + setNotifications(core.notifications); + setSavedObjects(core.savedObjects); + setData(data); + } +} diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts new file mode 100644 index 0000000000000..e349cfbdc0024 --- /dev/null +++ b/src/plugins/vis_type_vega/public/services.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +import { SavedObjectsStart } from 'kibana/public'; +import { NotificationsStart, IUiSettingsClient } from 'src/core/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { createGetterSetter } from '../../kibana_utils/public'; + +export const [getData, setData] = createGetterSetter('Data'); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); + +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); + +export const [getSavedObjects, setSavedObjects] = createGetterSetter( + 'SavedObjects' +); + +export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ + esShardTimeout: number; + enableExternalUrls: boolean; + emsTileLayerId: unknown; +}>('InjectedVars'); + +export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; +export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; +export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts rename to src/plugins/vis_type_vega/public/vega_fn.ts index 2a0da81a31a96..6d45e043f7cee 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -19,11 +19,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts similarity index 84% rename from src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts rename to src/plugins/vis_type_vega/public/vega_request_handler.ts index f63efc0007c3b..196e8fdcbafda 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -17,9 +17,7 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getSearchService } from '../../../../plugins/data/public/services'; -import { Filter, esQuery, TimeRange, Query } from '../../../../plugins/data/public'; +import { Filter, esQuery, TimeRange, Query } from '../../data/public'; // @ts-ignore import { VegaParser } from './data_model/vega_parser'; @@ -30,6 +28,7 @@ import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; import { VisParams } from './vega_fn'; +import { getData } from './services'; interface VegaRequestHandlerParams { query: Query; @@ -43,12 +42,18 @@ export function createVegaRequestHandler({ core: { uiSettings }, serviceSettings, }: VegaVisualizationDependencies) { - const { esClient } = getSearchService().__LEGACY; - const searchCache = new SearchCache(esClient, { max: 10, maxAge: 4 * 1000 }); + let searchCache: SearchCache | undefined; const { timefilter } = data.query.timefilter; const timeCache = new TimeCache(timefilter, 3 * 1000); return ({ timeRange, filters, query, visParams }: VegaRequestHandlerParams) => { + if (!searchCache) { + searchCache = new SearchCache(getData().search.__LEGACY.esClient, { + max: 10, + maxAge: 4 * 1000, + }); + } + timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts similarity index 92% rename from src/legacy/core_plugins/vis_type_vega/public/vega_type.ts rename to src/plugins/vis_type_vega/public/vega_type.ts index f56d7682efc6f..c864553c118b9 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,10 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js similarity index 99% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index c90f059ff7c94..be98d2b69ad87 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -26,7 +26,7 @@ import { Utils } from '../data_model/utils'; import { VISUALIZATION_COLORS } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; -import { esFilters } from '../../../../../plugins/data/public'; +import { esFilters } from '../../../data/public'; import { getEnableExternalUrls } from '../services'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js index d43eb9c3351ea..8e4009eab8488 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -19,7 +19,7 @@ import L from 'leaflet'; import 'leaflet-vega'; -import { KibanaMapLayer } from '../../../../../plugins/maps_legacy/public'; +import { KibanaMapLayer } from '../../../maps_legacy/public'; export class VegaMapLayer extends KibanaMapLayer { constructor(spec, options) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js similarity index 98% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 03aef29dc5739..bd6652a597203 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -21,7 +21,7 @@ import * as vega from 'vega-lib'; import { i18n } from '@kbn/i18n'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; +import { KibanaMap } from '../../../maps_legacy/public'; import { getEmsTileLayerId, getUISettings } from '../services'; export class VegaMapView extends VegaBaseView { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js rename to src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_view.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js rename to src/plugins/vis_type_vega/public/vega_visualization.js diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index ffb028ff131b3..1c545bb36cff0 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -33,7 +33,6 @@ import { EmbeddableInput, EmbeddableOutput, Embeddable, - EmbeddableVisTriggerContext, IContainer, } from '../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public'; @@ -261,7 +260,7 @@ export class VisualizeEmbeddable extends Embeddable { if (!this.eventsSubject) return; - this.eventsSubject.next({ name: 'filterBucket', data }); + this.eventsSubject.next({ + name: 'filterBucket', + data: data.data + ? { + data: data.data, + negate: data.negate, + } + : { data: [data] }, + }); }, brush: (data: any) => { if (!this.eventsSubject) return; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 7df420e7ba585..e475684ed5934 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -43,6 +43,8 @@ export type VisualizeEmbeddableContract = PublicContract; export { VisualizeInput } from './embeddable'; export type ExprVis = ExprVisClass; export { SchemaConfig } from './legacy/build_pipeline'; +// @ts-ignore +export { updateOldState } from './legacy/vis_update_state'; export { PersistedState } from './persisted_state'; export { VisualizationController, diff --git a/src/plugins/visualizations/public/legacy/vis_update.js b/src/plugins/visualizations/public/legacy/vis_update.js deleted file mode 100644 index 338a322e6aa57..0000000000000 --- a/src/plugins/visualizations/public/legacy/vis_update.js +++ /dev/null @@ -1,57 +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. - */ - -// TODO: this should be moved to vis_update_state -// Currently the migration takes place in Vis when calling setCurrentState. -// It should rather convert the raw saved object before starting to instantiate -// any JavaScript classes from it. -const updateVisualizationConfig = (stateConfig, config) => { - if (!stateConfig || stateConfig.seriesParams) return; - if (!['line', 'area', 'histogram'].includes(config.type)) return; - - // update value axis options - const isUserDefinedYAxis = config.setYExtents; - const defaultYExtents = config.defaultYExtents; - const mode = ['stacked', 'overlap'].includes(config.mode) ? 'normal' : config.mode || 'normal'; - config.valueAxes[0].scale = { - ...config.valueAxes[0].scale, - type: config.scale || 'linear', - setYExtents: config.setYExtents || false, - defaultYExtents: config.defaultYExtents || false, - boundsMargin: defaultYExtents ? config.boundsMargin : 0, - min: isUserDefinedYAxis ? config.yAxis.min : undefined, - max: isUserDefinedYAxis ? config.yAxis.max : undefined, - mode: mode, - }; - - // update series options - const interpolate = config.smoothLines ? 'cardinal' : config.interpolate; - const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(config.mode); - config.seriesParams[0] = { - ...config.seriesParams[0], - type: config.type || 'line', - mode: stacked ? 'stacked' : 'normal', - interpolate: interpolate, - drawLinesBetweenPoints: config.drawLinesBetweenPoints, - showCircles: config.showCircles, - radiusRatio: config.radiusRatio, - }; -}; - -export { updateVisualizationConfig }; diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js index 45610701e08c8..e345b9e5b8c9a 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.js +++ b/src/plugins/visualizations/public/legacy/vis_update_state.js @@ -75,6 +75,77 @@ function convertDateHistogramScaleMetrics(visState) { } } +function convertSeriesParams(visState) { + if (visState.params.seriesParams) { + return; + } + + // update value axis options + const isUserDefinedYAxis = visState.params.setYExtents; + const defaultYExtents = visState.params.defaultYExtents; + const mode = ['stacked', 'overlap'].includes(visState.params.mode) + ? 'normal' + : visState.params.mode || 'normal'; + + if (!visState.params.valueAxes || !visState.params.valueAxes.length) { + visState.params.valueAxes = [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ]; + } + + visState.params.valueAxes[0].scale = { + ...visState.params.valueAxes[0].scale, + type: visState.params.scale || 'linear', + setYExtents: visState.params.setYExtents || false, + defaultYExtents: visState.params.defaultYExtents || false, + boundsMargin: defaultYExtents ? visState.params.boundsMargin : 0, + min: isUserDefinedYAxis ? visState.params.yAxis.min : undefined, + max: isUserDefinedYAxis ? visState.params.yAxis.max : undefined, + mode: mode, + }; + + // update series options + const interpolate = visState.params.smoothLines ? 'cardinal' : visState.params.interpolate; + const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(visState.params.mode); + visState.params.seriesParams = [ + { + show: true, + type: visState.params.type || 'line', + mode: stacked ? 'stacked' : 'normal', + interpolate: interpolate, + drawLinesBetweenPoints: visState.params.drawLinesBetweenPoints, + showCircles: visState.params.showCircles, + radiusRatio: visState.params.radiusRatio, + data: { + label: 'Count', + id: '1', + }, + lineWidth: 2, + valueAxis: 'ValueAxis-1', + }, + ]; +} + /** * This function is responsible for updating old visStates - the actual saved object * object - into the format, that will be required by the current Kibana version. @@ -90,6 +161,10 @@ export const updateOldState = visState => { convertPropertyNames(newState); convertDateHistogramScaleMetrics(newState); + if (visState.params && ['line', 'area', 'histogram'].includes(visState.params.type)) { + convertSeriesParams(newState); + } + if (visState.type === 'gauge' && visState.fontSize) { delete newState.fontSize; _.set(newState, 'gauge.style.fontSize', visState.fontSize); diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index c99c7a4c2caa1..8bc98ca4b4784 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -32,7 +32,7 @@ import { // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public'; +import { IIndexPattern, ISearchSource } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../../plugins/discover/public'; import { getChrome, getOverlays, getIndexPatterns, getSavedObjects, getSearch } from '../services'; @@ -80,20 +80,24 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => { + const search = getSearch(); + const searchSource = inputSearchSource.createCopy ? inputSearchSource.createCopy() - : new SearchSource({ ...(inputSearchSource as any).fields }); + : search.searchSource.create({ ...(inputSearchSource as any).fields }); + if (savedSearchId) { const savedSearch = await createSavedSearchesLoader({ + search, savedObjectsClient: getSavedObjects().client, indexPatterns: getIndexPatterns(), - search: getSearch(), chrome: getChrome(), overlays: getOverlays(), }).get(savedSearchId); searchSource.setParent(savedSearch.searchSource); } + searchSource!.setField('size', 0); return searchSource; }; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 3cab4faf2a27f..009dd71b9a912 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -29,8 +29,6 @@ import { isFunction, defaults, cloneDeep } from 'lodash'; import { PersistedState } from './persisted_state'; -// @ts-ignore -import { updateVisualizationConfig } from './legacy/vis_update'; import { getTypes, getAggs } from './services'; import { VisType } from './vis_types'; import { @@ -121,9 +119,6 @@ export class Vis { this.params = this.getParams(state.params); } - // move to migration script - updateVisualizationConfig(state.params, this.params); - if (state.data && state.data.searchSource) { this.data.searchSource = state.data.searchSource!; this.data.indexPattern = this.data.searchSource.getField('index'); diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 860150d688711..6aef2c2b2ceb7 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -1214,7 +1214,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` data-test-subj="visNewDialogTypes" role="menu" > -
- - + - - + - + @@ -2669,7 +2669,7 @@ exports[`NewVisModal should render as expected 1`] = ` data-test-subj="visNewDialogTypes" role="menu" > - - - + - - + - + diff --git a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx index bb5037545cc82..acf562d5bf363 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx @@ -27,7 +27,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiKeyPadMenu, - EuiKeyPadMenuItemButton, + EuiKeyPadMenuItem, EuiModalHeader, EuiModalHeaderTitle, EuiScreenReaderOnly, @@ -262,7 +262,7 @@ class TypeSelection extends React.Component{visType.title}
} onClick={onClick} @@ -282,7 +282,7 @@ class TypeSelection extends React.Component - + ); }; diff --git a/src/plugins/visualize/public/application/legacy_app.js b/src/plugins/visualize/public/application/legacy_app.js index 7c5e3ce9408f0..c7cc11c1f3ff5 100644 --- a/src/plugins/visualize/public/application/legacy_app.js +++ b/src/plugins/visualize/public/application/legacy_app.js @@ -21,11 +21,7 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { - createKbnUrlStateStorage, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../kibana_utils/public'; +import { createKbnUrlStateStorage, redirectWhenMissing } from '../../../kibana_utils/public'; import { createSavedSearchesLoader } from '../../../discover/public'; import editorTemplate from './editor/editor.html'; @@ -127,7 +123,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => false, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { @@ -138,7 +134,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => true, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.CREATE_PATH, { @@ -147,7 +143,7 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getCreateBreadcrumbs, resolve: { resolved: function($route, history) { - const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; + const { data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; @@ -164,7 +160,8 @@ export function initVisualizeApp(app, deps) { ); } - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params)) .then(getResolvedResults(deps)) .catch( @@ -183,9 +180,10 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getEditBreadcrumbs, resolve: { resolved: function($route, history) { - const { chrome, core, data, savedVisualizations, toastNotifications } = deps; + const { chrome, data, savedVisualizations, toastNotifications } = deps; - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { chrome.recentlyAccessed.add(savedVis.getFullPath(), savedVis.title, savedVis.id); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index ab64e083a553d..df8479bc891b8 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -123,6 +123,8 @@ export class VisualizePlugin }; setServices(deps); + // make sure the index pattern list is up to date + await pluginsStart.data.indexPatterns.clearCache(); const { renderApp } = await import('./application/application'); const unmount = renderApp(params.element, params.appBasePath, deps); return () => { diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 4e106ef3e039a..1ec7c831b4864 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -53,6 +53,8 @@ module.exports = function(grunt) { function getKarmaFiles(shardNum) { return [ 'http://localhost:5610/test_bundle/built_css.css', + // Sets global variables normally set by the bootstrap.js script + 'http://localhost:5610/test_bundle/karma/globals.js', ...UiSharedDeps.jsDepFilenames.map( chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 086b13ecee2b3..0168626d4a1a9 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -33,7 +33,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { ['geo.src', 'IN'], ]; - describe('Discover', () => { + // FLAKY: https://github.com/elastic/kibana/issues/62497 + describe.skip('Discover', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/examples/config.js b/test/examples/config.js index 49d75da286075..2be34459d8d06 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -28,6 +28,7 @@ export default async function({ readConfigFile }) { require.resolve('./search'), require.resolve('./embeddables'), require.resolve('./ui_actions'), + require.resolve('./state_sync'), ], services: { ...functionalConfig.get('services'), diff --git a/test/examples/state_sync/index.ts b/test/examples/state_sync/index.ts new file mode 100644 index 0000000000000..3c524f0feb619 --- /dev/null +++ b/test/examples/state_sync/index.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function({ + getService, + getPageObjects, + loadTestFile, +}: PluginFunctionalProviderContext) { + const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); + + describe('state sync examples', function() { + before(async () => { + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('settings'); + }); + + loadTestFile(require.resolve('./todo_app')); + }); +} diff --git a/test/examples/state_sync/todo_app.ts b/test/examples/state_sync/todo_app.ts new file mode 100644 index 0000000000000..4933d746ca4fd --- /dev/null +++ b/test/examples/state_sync/todo_app.ts @@ -0,0 +1,189 @@ +/* + * 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. + */ + +import expect from '@kbn/expect'; + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const retry = getService('retry'); + const appsMenu = getService('appsMenu'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); + const log = getService('log'); + + describe('TODO app', () => { + describe("TODO app with browser history (platform's ScopedHistory)", async () => { + const appId = 'stateContainersExampleBrowserHistory'; + let base: string; + + before(async () => { + base = await PageObjects.common.getHostPort(); + await appsMenu.clickLink('State containers example - browser history routing'); + }); + + it('links are rendered correctly and state is preserved in links', async () => { + const getHrefByLinkTestSubj = async (linkTestSubj: string) => + (await testSubjects.find(linkTestSubj)).getAttribute('href'); + + await expectPathname(await getHrefByLinkTestSubj('filterLinkCompleted'), '/completed'); + await expectPathname( + await getHrefByLinkTestSubj('filterLinkNotCompleted'), + '/not-completed' + ); + await expectPathname(await getHrefByLinkTestSubj('filterLinkAll'), '/'); + }); + + it('TODO app state is synced with url, back navigation works', async () => { + // checking that in initial state checkbox is unchecked and state is synced with url + expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + + // check the checkbox by clicking the label (clicking checkbox directly fails as it is "no intractable") + (await find.byCssSelector('label[for="0"]')).click(); + + // wait for react to update dom and checkbox in checked state + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + // checking that url is updated with checked state + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + + // checking back and forward button + await browser.goBack(); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + }); + + await browser.goForward(); + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + }); + + it('links navigation works', async () => { + // click link to filter only not completed + await testSubjects.click('filterLinkNotCompleted'); + await expectPathname(await browser.getCurrentUrl(), '/not-completed'); + // checkbox should be missing because it is "completed" + await testSubjects.missingOrFail('todoCheckbox-0'); + }); + + /** + * Parses app's scoped pathname from absolute url and asserts it against `expectedPathname` + * Also checks that hashes are equal (detail of todo app that state is rendered in links) + * @param absoluteUrl + * @param expectedPathname + */ + async function expectPathname(absoluteUrl: string, expectedPathname: string) { + const scoped = await getScopedUrl(absoluteUrl); + const [pathname, newHash] = scoped.split('#'); + expect(pathname).to.be(expectedPathname); + const [, currentHash] = (await browser.getCurrentUrl()).split('#'); + expect(newHash.replace(/%27/g, "'")).to.be(currentHash.replace(/%27/g, "'")); + } + + /** + * Get's part of url scoped to this app (removed kibana's host and app's pathname) + * @param url - absolute url + */ + async function getScopedUrl(url: string): Promise { + expect(url).to.contain(base); + expect(url).to.contain(appId); + const scopedUrl = url.slice(url.indexOf(appId) + appId.length); + expect(scopedUrl).not.to.contain(appId); // app id in url only once + return scopedUrl; + } + }); + + describe('TODO app with hash history ', async () => { + before(async () => { + await appsMenu.clickLink('State containers example - hash history routing'); + }); + + it('Links are rendered correctly and state is preserved in links', async () => { + const getHrefByLinkTestSubj = async (linkTestSubj: string) => + (await testSubjects.find(linkTestSubj)).getAttribute('href'); + await expectHashPathname(await getHrefByLinkTestSubj('filterLinkCompleted'), '/completed'); + await expectHashPathname( + await getHrefByLinkTestSubj('filterLinkNotCompleted'), + '/not-completed' + ); + await expectHashPathname(await getHrefByLinkTestSubj('filterLinkAll'), '/'); + }); + + it('TODO app state is synced with url, back navigation works', async () => { + // checking that in initial state checkbox is unchecked and state is synced with url + expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + // check the checkbox by clicking the label (clicking checkbox directly fails as it is "no intractable") + (await find.byCssSelector('label[for="0"]')).click(); + + // wait for react to update dom and checkbox in checked state + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + // checking that url is updated with checked state + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + + // checking back and forward button + await browser.goBack(); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + }); + + await browser.goForward(); + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + }); + + it('links navigation works', async () => { + // click link to filter only not completed + await testSubjects.click('filterLinkNotCompleted'); + await expectHashPathname(await browser.getCurrentUrl(), '/not-completed'); + // checkbox should be missing because it is "completed" + await testSubjects.missingOrFail('todoCheckbox-0'); + }); + + /** + * Parses app's pathname in hash from absolute url and asserts it against `expectedPathname` + * Also checks that queries in hashes are equal (detail of todo app that state is rendered in links) + * @param absoluteUrl + * @param expectedPathname + */ + async function expectHashPathname(hash: string, expectedPathname: string) { + log.debug(`expect hash pathname ${hash} to be ${expectedPathname}`); + const hashPath = hash.split('#')[1]; + const [hashPathname, hashQuery] = hashPath.split('?'); + const [, currentHash] = (await browser.getCurrentUrl()).split('#'); + const [, currentHashQuery] = currentHash.split('?'); + expect(currentHashQuery.replace(/%27/g, "'")).to.be(hashQuery.replace(/%27/g, "'")); + expect(hashPathname).to.be(expectedPathname); + } + }); + }); +} diff --git a/test/functional/apps/dashboard/dashboard_saved_query.js b/test/functional/apps/dashboard/dashboard_saved_query.js new file mode 100644 index 0000000000000..99d0aed082e70 --- /dev/null +++ b/test/functional/apps/dashboard/dashboard_saved_query.js @@ -0,0 +1,128 @@ +/* + * 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. + */ + +import expect from '@kbn/expect'; + +export default function({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']); + const browser = getService('browser'); + const queryBar = getService('queryBar'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); + const testSubjects = getService('testSubjects'); + + describe('dashboard saved queries', function describeIndexTests() { + before(async function() { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + }); + + describe('saved query management component functionality', function() { + before(async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('should show the saved query management component when there are no saved queries', async () => { + await savedQueryManagementComponent.openSavedQueryManagementComponent(); + const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); + expect(descriptionText).to.eql( + 'SAVED QUERIES\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' + ); + }); + + it('should allow a query to be saved via the saved objects management component', async () => { + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.savedQueryTextExist('response:200'); + }); + + it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + }); + + it('preserves the currently loaded query when the page is reloaded', async () => { + await browser.refresh(); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); + }); + + it('allows saving changes to a currently loaded query via the saved query management component', async () => { + await queryBar.setQuery('response:404'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'OkResponse', + '404 responses', + false, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows saving the currently loaded query as a new query', async () => { + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'OkResponseCopy', + '200 responses', + false, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponseCopy'); + }); + + it('allows deleting the currently loaded saved query in the saved query management component and clears the query', async () => { + await savedQueryManagementComponent.deleteSavedQuery('OkResponseCopy'); + await savedQueryManagementComponent.savedQueryMissingOrFail('OkResponseCopy'); + expect(await queryBar.getQueryString()).to.eql(''); + }); + + it('resets any changes to a loaded query on reloading the same saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.setQuery('response:503'); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows clearing the currently loaded saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/full_screen_mode.js b/test/functional/apps/dashboard/full_screen_mode.js index df00f64530ca0..17eb6d8f08a9c 100644 --- a/test/functional/apps/dashboard/full_screen_mode.js +++ b/test/functional/apps/dashboard/full_screen_mode.js @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['dashboard', 'common']); + const filterBar = getService('filterBar'); describe('full screen mode', () => { before(async () => { @@ -81,5 +82,22 @@ export default function({ getService, getPageObjects }) { expect(isChromeVisible).to.be(true); }); }); + + it('shows filter bar in fullscreen mode', async () => { + await filterBar.addFilter('bytes', 'is', '12345678'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.clickFullScreenMode(); + await retry.try(async () => { + const isChromeHidden = await PageObjects.common.isChromeHidden(); + expect(isChromeHidden).to.be(true); + }); + expect(await filterBar.getFilterCount()).to.be(1); + await PageObjects.dashboard.clickExitFullScreenLogoButton(); + await retry.try(async () => { + const isChromeVisible = await PageObjects.common.isChromeVisible(); + expect(isChromeVisible).to.be(true); + }); + await filterBar.removeFilter('bytes'); + }); }); } diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 6666ccc57d584..bd8e6812147e1 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -74,6 +74,7 @@ export default function({ getService, loadTestFile }) { loadTestFile(require.resolve('./panel_expand_toggle')); loadTestFile(require.resolve('./dashboard_grid')); loadTestFile(require.resolve('./view_edit')); + loadTestFile(require.resolve('./dashboard_saved_query')); // Order of test suites *shouldn't* be important but there's a bug for the view_edit test above // https://github.com/elastic/kibana/issues/46752 // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 76f3a3aea365f..9b50eeda20073 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -74,6 +74,7 @@ export default function({ getService, getPageObjects }) { true ); await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.savedQueryTextExist('response:200'); }); it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index a20d7ae9a5372..b76ce141a4418 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -215,6 +215,8 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide public async clickNewDashboard() { await listingTable.clickNewButton('createDashboardPromptButton'); + // make sure the dashboard page is shown + await this.waitForRenderComplete(); } public async clickCreateDashboardPrompt() { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 6dcd017335c85..8864eda3823ef 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -206,15 +206,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getFieldsTabCount() { return retry.try(async () => { - const text = await testSubjects.getVisibleText('tab-count-indexedFields'); - return text.replace(/\((.*)\)/, '$1'); + const indexedFieldsTab = await find.byCssSelector('#indexedFields .euiTab__content'); + const text = await indexedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } async getScriptedFieldsTabCount() { return await retry.try(async () => { - const theText = await testSubjects.getVisibleText('tab-count-scriptedFields'); - return theText.replace(/\((.*)\)/, '$1'); + const scriptedFieldsTab = await find.byCssSelector('#scriptedFields .euiTab__content'); + const text = await scriptedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } @@ -241,13 +243,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async setFieldTypeFilter(type: string) { await find.clickByCssSelector( - 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[label="' + type + '"]' + 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[value="' + type + '"]' ); } async setScriptedFieldLanguageFilter(language: string) { await find.clickByCssSelector( - 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[label="' + + 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[value="' + language + '"]' ); @@ -412,17 +414,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickFieldsTab() { log.debug('click Fields tab'); - await testSubjects.click('tab-indexFields'); + await find.clickByCssSelector('#indexedFields'); } async clickScriptedFieldsTab() { log.debug('click Scripted Fields tab'); - await testSubjects.click('tab-scriptedFields'); + await find.clickByCssSelector('#scriptedFields'); } async clickSourceFiltersTab() { log.debug('click Source Filters tab'); - await testSubjects.click('tab-sourceFilters'); + await find.clickByCssSelector('#sourceFilters'); } async editScriptedField(name: string) { diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index 244c1cd214de5..66bf15f3da53c 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -151,6 +151,12 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide await testSubjects.existOrFail(`~load-saved-query-${title}-button`); } + async savedQueryTextExist(text: string) { + await this.openSavedQueryManagementComponent(); + const queryString = await queryBar.getQueryString(); + expect(queryString).to.eql(text); + } + async savedQueryMissingOrFail(title: string) { await retry.try(async () => { await this.openSavedQueryManagementComponent(); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 611e16e5a942d..001293be44829 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index 914ff39884fa3..4ca705137a12c 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 9ee7845816faa..818621ffe5ae5 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 3b3d69c06ff3e..59b7337f5ff07 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index c8715ac3447bd..a8e50e017102f 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -8,14 +8,14 @@ "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": "legacy/plugins/beats_management", "xpack.canvas": "legacy/plugins/canvas", - "xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication", + "xpack.crossClusterReplication": "plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", "xpack.data": "plugins/data_enhanced", "xpack.drilldowns": "plugins/drilldowns", "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", - "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], + "xpack.graph": ["plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 3068cdd0daa5b..af5ace8e3cd3b 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -32,6 +32,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, + '^(!!)?file-loader!': fileMockPath, }, coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], diff --git a/x-pack/index.js b/x-pack/index.js index 7fbd992120ea6..82a35f1710361 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,12 +5,10 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { graph } from './legacy/plugins/graph'; import { monitoring } from './legacy/plugins/monitoring'; import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; -import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; @@ -22,7 +20,6 @@ import { taskManager } from './legacy/plugins/task_manager'; import { rollup } from './legacy/plugins/rollup'; import { siem } from './legacy/plugins/siem'; import { remoteClusters } from './legacy/plugins/remote_clusters'; -import { crossClusterReplication } from './legacy/plugins/cross_cluster_replication'; import { upgradeAssistant } from './legacy/plugins/upgrade_assistant'; import { uptime } from './legacy/plugins/uptime'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; @@ -34,13 +31,11 @@ import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; module.exports = function(kibana) { return [ xpackMain(kibana), - graph(kibana), monitoring(kibana), reporting(kibana), spaces(kibana), security(kibana), dashboardMode(kibana), - logstash(kibana), beats(kibana), apm(kibana), maps(kibana), @@ -51,7 +46,6 @@ module.exports = function(kibana) { rollup(kibana), siem(kibana), remoteClusters(kibana), - crossClusterReplication(kibana), upgradeAssistant(kibana), uptime(kibana), encryptedSavedObjects(kibana), diff --git a/x-pack/legacy/plugins/actions/server/index.ts b/x-pack/legacy/plugins/actions/server/index.ts index 7235eda88149f..63dd6f99f9c24 100644 --- a/x-pack/legacy/plugins/actions/server/index.ts +++ b/x-pack/legacy/plugins/actions/server/index.ts @@ -6,8 +6,13 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; import mappings from './mappings.json'; +import { + LegacyPluginApi, + LegacyPluginSpec, + ArrayOrItem, +} from '../../../../../src/legacy/plugin_discovery/types'; -export function actions(kibana: any) { +export function actions(kibana: LegacyPluginApi): ArrayOrItem { return new kibana.Plugin({ id: 'actions', configPrefix: 'xpack.actions', @@ -29,5 +34,5 @@ export function actions(kibana: any) { uiExports: { mappings, }, - }); + } as Legacy.PluginSpecOptions); } diff --git a/x-pack/legacy/plugins/alerting/server/index.ts b/x-pack/legacy/plugins/alerting/server/index.ts index 5bf7cda51bda6..065af7dedebd9 100644 --- a/x-pack/legacy/plugins/alerting/server/index.ts +++ b/x-pack/legacy/plugins/alerting/server/index.ts @@ -7,8 +7,13 @@ import { Legacy } from 'kibana'; import { Root } from 'joi'; import mappings from './mappings.json'; +import { + LegacyPluginApi, + LegacyPluginSpec, + ArrayOrItem, +} from '../../../../../src/legacy/plugin_discovery/types'; -export function alerting(kibana: any) { +export function alerting(kibana: LegacyPluginApi): ArrayOrItem { return new kibana.Plugin({ id: 'alerting', configPrefix: 'xpack.alerting', @@ -31,5 +36,5 @@ export function alerting(kibana: any) { uiExports: { mappings, }, - }); + } as Legacy.PluginSpecOptions); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index afa0cb51cd108..0fbf0a5c7a27d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -141,6 +141,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > Group ID @@ -162,6 +163,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > Type @@ -183,6 +185,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > Error message and culprit @@ -231,6 +234,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > Occurrences @@ -272,6 +276,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > Latest occurrence @@ -519,6 +524,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > Group ID @@ -540,6 +546,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > Type @@ -561,6 +568,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > Error message and culprit @@ -609,6 +617,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > Occurrences @@ -650,6 +659,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > Latest occurrence diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index 31c227d8bbcab..de775dbc8162a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -75,8 +75,22 @@ storiesOf('app/ServiceMap/Cytoscape', module) const cy = cytoscape(); const elements = [ { data: { id: 'default' } }, + { + data: { + id: 'aws', + 'span.type': 'aws', + 'span.subtype': 'servicename' + } + }, { data: { id: 'cache', 'span.type': 'cache' } }, { data: { id: 'database', 'span.type': 'db' } }, + { + data: { + id: 'cassandra', + 'span.type': 'db', + 'span.subtype': 'cassandra' + } + }, { data: { id: 'elasticsearch', @@ -84,9 +98,87 @@ storiesOf('app/ServiceMap/Cytoscape', module) 'span.subtype': 'elasticsearch' } }, + { + data: { + id: 'mongodb', + 'span.type': 'db', + 'span.subtype': 'mongodb' + } + }, + { + data: { + id: 'mysql', + 'span.type': 'db', + 'span.subtype': 'mysql' + } + }, + { + data: { + id: 'postgresql', + 'span.type': 'db', + 'span.subtype': 'postgresql' + } + }, + { + data: { + id: 'redis', + 'span.type': 'db', + 'span.subtype': 'redis' + } + }, { data: { id: 'external', 'span.type': 'external' } }, { data: { id: 'ext', 'span.type': 'ext' } }, + { + data: { + id: 'graphql', + 'span.type': 'external', + 'span.subtype': 'graphql' + } + }, + { + data: { + id: 'grpc', + 'span.type': 'external', + 'span.subtype': 'grpc' + } + }, + { + data: { + id: 'websocket', + 'span.type': 'external', + 'span.subtype': 'websocket' + } + }, { data: { id: 'messaging', 'span.type': 'messaging' } }, + { + data: { + id: 'jms', + 'span.type': 'messaging', + 'span.subtype': 'jms' + } + }, + { + data: { + id: 'kafka', + 'span.type': 'messaging', + 'span.subtype': 'kafka' + } + }, + { data: { id: 'template', 'span.type': 'template' } }, + { + data: { + id: 'handlebars', + 'span.type': 'template', + 'span.subtype': 'handlebars' + } + }, + { + data: { + id: 'dark', + 'service.name': 'dark service', + 'agent.name': 'dark' + } + }, { data: { id: 'dotnet', @@ -159,11 +251,13 @@ storiesOf('app/ServiceMap/Cytoscape', module) - agent.name: {node.data('agent.name') || 'undefined'}, - span.type: {node.data('span.type') || 'undefined'}, + + agent.name: {node.data('agent.name') || 'undefined'} +
+ span.type: {node.data('span.type') || 'undefined'} +
span.subtype: {node.data('span.subtype') || 'undefined'} - +
} icon={ + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg new file mode 100644 index 0000000000000..0cc2710563958 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dark.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dark.svg new file mode 100644 index 0000000000000..9ae4b31c1a0d6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg new file mode 100644 index 0000000000000..16dd58cd53184 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg new file mode 100644 index 0000000000000..768294776f382 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg new file mode 100644 index 0000000000000..b8e808baa1ac1 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg new file mode 100644 index 0000000000000..ed3f00b0dadf2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg new file mode 100644 index 0000000000000..bb73f9a5ea90b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg new file mode 100644 index 0000000000000..77e4e9b3b5ff8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg @@ -0,0 +1,4 @@ + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg new file mode 100644 index 0000000000000..fcae8d10013da --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/redis.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/redis.svg new file mode 100644 index 0000000000000..907312ebe74e6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/redis.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg new file mode 100644 index 0000000000000..2c83babd0bac1 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 0c04b7cccbd23..e5c20b260e097 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -35,7 +35,7 @@ const data = [ ]; describe('CustomLink', () => { - let callApmApiSpy: Function; + let callApmApiSpy: jasmine.Spy; beforeAll(() => { callApmApiSpy = spyOn(apmApi, 'callApmApi').and.returnValue({}); }); @@ -101,7 +101,7 @@ describe('CustomLink', () => { ]); }); - it('checks if create custom link button is available and working', () => { + it('checks if create custom link button is available and working', async () => { const { queryByText, getByText } = render( @@ -113,6 +113,7 @@ describe('CustomLink', () => { act(() => { fireEvent.click(getByText('Create custom link')); }); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); expect(queryByText('Create link')).toBeInTheDocument(); }); }); @@ -144,8 +145,10 @@ describe('CustomLink', () => { act(() => { fireEvent.click(component.getByText('Create custom link')); }); - await wait(() => component.queryByText('Create link')); - expect(component.queryByText('Create link')).toBeInTheDocument(); + await wait(() => + expect(component.queryByText('Create link')).toBeInTheDocument() + ); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); return component; }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index 87102a486ab5f..27095264461ee 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -74,7 +74,7 @@ function getSpanTypes(span: Span) { const SpanBadge = styled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -`; +` as any; const HttpInfoContainer = styled('div')` margin-right: ${px(units.quarter)}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx index 764f15f943ad2..a5d8902ff1626 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx @@ -13,7 +13,7 @@ import { px, units } from '../../../../../../style/variables'; const SpanBadge = styled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -`; +` as any; interface SyncBadgeProps { /** diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index d844ac8b5988d..e0a01e9422c85 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -191,7 +191,7 @@ describe('waterfall_helpers', () => { name: 'SELECT FROM products', id: 'mySpanIdB' }, - child_ids: ['mySpanIdA', 'mySpanIdC'] + child: { id: ['mySpanIdA', 'mySpanIdC'] } } as Span, { parent: { id: 'mySpanIdD' }, @@ -294,7 +294,7 @@ describe('waterfall_helpers', () => { name: 'SELECT FROM products', id: 'mySpanIdB' }, - child_ids: ['incorrectId', 'mySpanIdC'] + child: { id: ['incorrectId', 'mySpanIdC'] } } as Span, { parent: { id: 'mySpanIdD' }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 8a873b2ddf1c9..73193cc7c9dbb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -237,7 +237,7 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => }); /** - * Changes the parent_id of items based on the child_ids property. + * Changes the parent_id of items based on the child.id property. * Solves the problem of Inferred spans that are created as child of trace spans * when it actually should be its parent. * @param waterfallItems @@ -245,10 +245,10 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => const reparentSpans = (waterfallItems: IWaterfallItem[]) => { return waterfallItems.map(waterfallItem => { if (waterfallItem.docType === 'span') { - const { child_ids: childIds } = waterfallItem.doc; - if (childIds) { - childIds.forEach(childId => { - const item = waterfallItems.find(_item => _item.id === childId); + const childId = waterfallItem.doc.child?.id; + if (childId) { + childId.forEach(id => { + const item = waterfallItems.find(_item => _item.id === id); if (item) { item.parentId = waterfallItem.id; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index 306c8e4f3fedb..2f28e37f73f62 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -2027,7 +2027,7 @@ export const inferredSpans = { id: '41226ae63af4f235', type: 'unknown' }, - child_ids: ['8d80de06aa11a6fc'] + child: { ids: ['8d80de06aa11a6fc'] } }, { container: { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap index 0aeb2443679fa..4177b54f20385 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap @@ -192,7 +192,9 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] />
- + -
-
+ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 7558f002c0afc..858e6d29bfa5e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -15,9 +15,9 @@ interface Props { count: number; } -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` margin-top: ${px(units.eighth)}; -`; +` as any) as any; export const ErrorCountSummaryItemBadge = ({ count }: Props) => ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx index 94ae9e63644b9..1e1d49b2cf417 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx @@ -11,9 +11,9 @@ import styled from 'styled-components'; import { units, px, truncate, unit } from '../../../../style/variables'; import { HttpStatusBadge } from '../HttpStatusBadge'; -const HttpInfoBadge = styled(EuiBadge)` +const HttpInfoBadge = (styled(EuiBadge)` margin-right: ${px(units.quarter)}; -`; +` as any) as any; const Url = styled('span')` display: inline-block; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index ce42bd3e39ad1..8dc2076eab5b5 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render, fireEvent, act } from '@testing-library/react'; +import { render, fireEvent, act, wait } from '@testing-library/react'; import { TransactionActionMenu } from '../TransactionActionMenu'; import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; import * as Transactions from './mockData'; @@ -143,8 +143,9 @@ describe('TransactionActionMenu component', () => { }); describe('Custom links', () => { + let callApmApiSpy: jasmine.Spy; beforeAll(() => { - spyOn(apmApi, 'callApmApi').and.returnValue({}); + callApmApiSpy = spyOn(apmApi, 'callApmApi').and.returnValue({}); }); afterAll(() => { jest.resetAllMocks(); @@ -257,7 +258,7 @@ describe('TransactionActionMenu component', () => { }); expectTextsInDocument(component, ['Custom Links']); }); - it('opens flyout with filters prefilled', () => { + it('opens flyout with filters prefilled', async () => { const license = new License({ signature: 'test signature', license: { @@ -287,6 +288,7 @@ describe('TransactionActionMenu component', () => { fireEvent.click(component.getByText('Create custom link')); }); expectTextsInDocument(component, ['Create link']); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); const getFilterKeyValue = (key: string) => { return { [(component.getAllByText(key)[0] as HTMLOptionElement) diff --git a/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx b/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx index be8a56486577e..7fa0231cf3409 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx @@ -31,7 +31,7 @@ export const TagBadge = (props: TagBadgeProps) => { } onClickAriaLabel={onClickAriaLabel} > {idToRender} diff --git a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js index 72b0b8f0e533f..a81483d1e7a17 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js @@ -7,6 +7,7 @@ import path from 'path'; import moment from 'moment'; import 'moment-timezone'; +import ReactDOM from "react-dom"; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; @@ -24,6 +25,9 @@ moment.tz.setDefault('UTC'); const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 Date.now = jest.fn(() => testTime); +// Mock telemetry service +jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => { } })); + // Mock EUI generated ids to be consistently predictable for snapshots. jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); @@ -32,7 +36,7 @@ jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `gene jest.mock('@elastic/eui/lib/components/code/code', () => { const React = require.requireActual('react'); return { - EuiCode: ({children, className}) => ( + EuiCode: ({ children, className }) => ( {children} @@ -61,6 +65,12 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }; }); + +// Mock React Portal for components that use modals, tooltips, etc +ReactDOM.createPortal = jest.fn((element) => { + return element; +}); + jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { htmlIdGenerator: () => () => `generated-id`, @@ -71,7 +81,7 @@ jest.mock('plugins/interpreter/registries', () => ({})); // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', () => { return 'Disabled Panel'; } diff --git a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js index cc74faeac6a96..963cf831ef698 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js @@ -177,8 +177,10 @@ module.exports = async ({ config }) => { }), // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin(/lib\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), new webpack.NormalModuleReplacementPlugin(/lib\/download_workpad/, path.resolve(__dirname, '../tasks/mocks/downloadWorkpad')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/custom_element_service/, path.resolve(__dirname, '../tasks/mocks/customElementService')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/ui_metric/, path.resolve(__dirname, '../tasks/mocks/uiMetric')), ); // Tell Webpack about relevant extensions diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png deleted file mode 100644 index 93456066429d9..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts index 0650ac15c656e..df829e8b97676 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const areaChart: ElementFactory = () => ({ name: 'areaChart', - displayName: 'Area chart', + displayName: 'Area', help: 'A line chart with a filled body', - tags: ['chart'], - image: header, + type: 'chart', + icon: 'visArea', expression: `filters | demodata | pointseries x="time" y="mean(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png deleted file mode 100644 index db541fe7c53b8..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts index 7ab510e419769..7ac1d0ac83b0b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts @@ -5,16 +5,15 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const bubbleChart: ElementFactory = () => ({ name: 'bubbleChart', - displayName: 'Bubble chart', - tags: ['chart'], + displayName: 'Bubble', + type: 'chart', help: 'A customizable bubble chart', width: 700, height: 300, - image: header, + icon: 'heatmap', expression: `filters | demodata | pointseries x="project" y="sum(price)" color="state" size="size(username)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png deleted file mode 100644 index 37ab329a49bb8..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts index 914982951d664..ec8477f8f1017 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const debug: ElementFactory = () => ({ name: 'debug', - displayName: 'Debug', - tags: ['text'], + displayName: 'Debug data', help: 'Just dumps the configuration of the element', - image: header, + icon: 'bug', expression: `demodata | render as=debug`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png deleted file mode 100644 index 4bbfb6f8f68fc..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts deleted file mode 100644 index 4ea8037d2073e..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts +++ /dev/null @@ -1,21 +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 { ElementFactory } from '../../../types'; -import header from './header.png'; - -export const donut: ElementFactory = () => ({ - name: 'donut', - displayName: 'Donut chart', - tags: ['chart', 'proportion'], - help: 'A customizable donut chart', - image: header, - expression: `filters -| demodata -| pointseries color="project" size="max(price)" -| pie hole=50 labels=false legend="ne" -| render`, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png deleted file mode 100644 index 727b4d23941fd..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts index bde223d2a606e..bb1c13ca618be 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const dropdownFilter: ElementFactory = () => ({ - name: 'dropdown_filter', - displayName: 'Dropdown filter', - tags: ['filter'], + name: 'dropdownFilter', + displayName: 'Dropdown select', + type: 'filter', help: 'A dropdown from which you can select values for an "exactly" filter', - image: header, + icon: 'filter', height: 50, expression: `demodata | dropdownControl valueColumn=project filterColumn=project | render`, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts new file mode 100644 index 0000000000000..35a4a75f49c4e --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { ElementFactory } from '../../../types'; + +export const filterDebug: ElementFactory = () => ({ + name: 'filterDebug', + displayName: 'Debug filter', + help: 'Shows the underlying global filters in a workpad', + icon: 'bug', + expression: `filters +| render as=debug`, +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png deleted file mode 100644 index 9b6ee47d88698..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts index 7fddf48c70385..9567336decd5d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalBarChart: ElementFactory = () => ({ name: 'horizontalBarChart', - displayName: 'Horizontal bar chart', - tags: ['chart'], + displayName: 'Bar horizontal', + type: 'chart', help: 'A customizable horizontal bar chart', - image: header, + icon: 'visBarHorizontal', expression: `filters | demodata | pointseries x="size(cost)" y="project" color="project" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png deleted file mode 100644 index f28ad4a3ce4be..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts index f4a50a007c5de..529a74893a5de 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalProgressBar: ElementFactory = () => ({ name: 'horizontalProgressBar', - displayName: 'Horizontal progress bar', - tags: ['chart', 'proportion'], + displayName: 'Horizontal bar', + type: 'progress', help: 'Displays progress as a portion of a horizontal bar', width: 400, height: 30, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png deleted file mode 100644 index 2eaeb2e976a78..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts index 9b3aea2e55324..d5eba32325d1a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalProgressPill: ElementFactory = () => ({ name: 'horizontalProgressPill', - displayName: 'Horizontal progress pill', - tags: ['chart', 'proportion'], + displayName: 'Horizontal pill', + type: 'progress', help: 'Displays progress as a portion of a horizontal pill', width: 400, height: 30, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png deleted file mode 100644 index 7f29fc64c36b9..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts index eec1e2af61aad..ed7f6a99ddc32 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const image: ElementFactory = () => ({ name: 'image', displayName: 'Image', - tags: ['graphic'], + type: 'image', help: 'A static image', - image: header, + icon: 'image', expression: `image dataurl=null mode="contain" | render`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts index feba88dbe8b90..ec3b8a7798be1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -8,15 +8,15 @@ import { applyElementStrings } from '../../i18n/elements'; import { areaChart } from './area_chart'; import { bubbleChart } from './bubble_chart'; import { debug } from './debug'; -import { donut } from './donut'; import { dropdownFilter } from './dropdown_filter'; +import { filterDebug } from './filter_debug'; import { horizontalBarChart } from './horizontal_bar_chart'; import { horizontalProgressBar } from './horizontal_progress_bar'; import { horizontalProgressPill } from './horizontal_progress_pill'; import { image } from './image'; import { lineChart } from './line_chart'; import { markdown } from './markdown'; -import { metric } from './metric'; +import { metricElementInitializer } from './metric'; import { pie } from './pie'; import { plot } from './plot'; import { progressGauge } from './progress_gauge'; @@ -26,25 +26,26 @@ import { repeatImage } from './repeat_image'; import { revealImage } from './reveal_image'; import { shape } from './shape'; import { table } from './table'; -import { tiltedPie } from './tilted_pie'; import { timeFilter } from './time_filter'; import { verticalBarChart } from './vert_bar_chart'; import { verticalProgressBar } from './vertical_progress_bar'; import { verticalProgressPill } from './vertical_progress_pill'; -export const elementSpecs = applyElementStrings([ +import { SetupInitializer } from '../plugin'; +import { ElementFactory } from '../../types'; + +const elementSpecs = [ areaChart, bubbleChart, debug, - donut, dropdownFilter, + filterDebug, image, horizontalBarChart, horizontalProgressBar, horizontalProgressPill, lineChart, markdown, - metric, pie, plot, progressGauge, @@ -54,9 +55,19 @@ export const elementSpecs = applyElementStrings([ revealImage, shape, table, - tiltedPie, timeFilter, verticalBarChart, verticalProgressBar, verticalProgressPill, -]); +]; + +const initializeElementFactories = [metricElementInitializer]; + +export const initializeElements: SetupInitializer = (core, plugins) => { + const specs = [ + ...elementSpecs, + ...initializeElementFactories.map(factory => factory(core, plugins)), + ]; + + return applyElementStrings(specs); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png deleted file mode 100644 index eea133ee3680b..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts index 5b8533eea65bc..d19ddeb00dd67 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const lineChart: ElementFactory = () => ({ name: 'lineChart', - displayName: 'Line chart', - tags: ['chart'], + displayName: 'Line', + type: 'chart', help: 'A customizable line chart', - image: header, + icon: 'visLine', expression: `filters | demodata | pointseries x="time" y="mean(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png deleted file mode 100644 index a8b8550f5baea..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts index 1c7013834cbe4..7b114daa11870 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import header from './header.png'; - import { ElementFactory } from '../../../types'; export const markdown: ElementFactory = () => ({ name: 'markdown', - displayName: 'Markdown', - tags: ['text'], - help: 'Markup from Markdown', - image: header, + displayName: 'Text', + type: 'text', + help: 'Add text using Markdown', + icon: 'visText', expression: `filters | demodata | markdown "### Welcome to the Markdown element diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png deleted file mode 100644 index 0510342cdc54a..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index c08c090f11f91..7256657903aab 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -5,24 +5,25 @@ */ import { openSans } from '../../../common/lib/fonts'; -import header from './header.png'; -import { getAdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; - import { ElementFactory } from '../../../types'; -export const metric: ElementFactory = () => ({ - name: 'metric', - displayName: 'Metric', - tags: ['text'], - help: 'A number with a label', - width: 200, - height: 100, - image: header, - expression: `filters -| demodata -| math "unique(country)" -| metric "Countries" - metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} - labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${getAdvancedSettings().get('format:number:defaultPattern')}" -| render`, -}); +import { SetupInitializer } from '../../plugin'; + +export const metricElementInitializer: SetupInitializer = (core, setup) => { + return () => ({ + name: 'metric', + displayName: 'Metric', + type: 'chart', + help: 'A number with a label', + width: 200, + height: 100, + icon: 'visMetric', + expression: `filters + | demodata + | math "unique(country)" + | metric "Countries" + metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} + labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${core.uiSettings.get('format:number:defaultPattern')}" + | render`, + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png deleted file mode 100644 index deecd1067427c..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts index cfb9031325254..b7606ea94bc9f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts @@ -4,17 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import header from './header.png'; - import { ElementFactory } from '../../../types'; export const pie: ElementFactory = () => ({ name: 'pie', - displayName: 'Pie chart', - tags: ['chart', 'proportion'], + displayName: 'Pie', + type: 'chart', width: 300, height: 300, help: 'A simple pie chart', - image: header, + icon: 'visPie', expression: `filters | demodata | pointseries color="state" size="max(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png deleted file mode 100644 index d48c789ae5a92..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts index dd1660d558667..8648b65def4b2 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const plot: ElementFactory = () => ({ name: 'plot', displayName: 'Coordinate plot', - tags: ['chart'], + type: 'chart', help: 'Mixed line, bar or dot charts', - image: header, expression: `filters | demodata | pointseries x="time" y="sum(price)" color="state" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png deleted file mode 100644 index 8340c8a53b6ce..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts index 4ec192fb787fe..b21b7df286ace 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts @@ -6,16 +6,15 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressGauge: ElementFactory = () => ({ name: 'progressGauge', - displayName: 'Progress gauge', - tags: ['chart', 'proportion'], + displayName: 'Gauge', + type: 'progress', help: 'Displays progress as a portion of a gauge', width: 200, height: 200, - image: header, + icon: 'visGoal', expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png deleted file mode 100644 index b5b708529edd4..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts index 91fcb24996bc0..9ccb9489e8306 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressSemicircle: ElementFactory = () => ({ name: 'progressSemicircle', - displayName: 'Progress semicircle', - tags: ['chart', 'proportion'], + displayName: 'Semicircle', + type: 'progress', help: 'Displays progress as a portion of a semicircle', width: 200, height: 100, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png deleted file mode 100644 index 71e5d7e29444e..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts index 05c537f88756b..42bf36346c303 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressWheel: ElementFactory = () => ({ name: 'progressWheel', - displayName: 'Progress wheel', - tags: ['chart', 'proportion'], + displayName: 'Wheel', + type: 'progress', help: 'Displays progress as a portion of a wheel', width: 200, height: 200, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png deleted file mode 100644 index 9843c9a6d02c0..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts index df79651620642..0bf0ec4c2dc1f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const repeatImage: ElementFactory = () => ({ name: 'repeatImage', displayName: 'Image repeat', - tags: ['graphic', 'proportion'], + type: 'image', help: 'Repeats an image N times', - image: header, expression: `filters | demodata | math "mean(cost)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png deleted file mode 100644 index 8dc33b5a7259e..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts index 01c66ed3a26ec..88000f6b66a91 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const revealImage: ElementFactory = () => ({ name: 'revealImage', displayName: 'Image reveal', - tags: ['graphic', 'proportion'], + type: 'image', help: 'Reveals a percentage of an image', - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png deleted file mode 100644 index 3212d47591c07..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts index 3f3954ff02b02..f922473fa818f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts @@ -5,16 +5,15 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const shape: ElementFactory = () => ({ name: 'shape', displayName: 'Shape', - tags: ['graphic'], + type: 'shape', help: 'A customizable shape', width: 200, height: 200, - image: header, + icon: 'node', expression: 'shape "square" fill="#4cbce4" border="rgba(255,255,255,0)" borderWidth=0 maintainAspect=false | render', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png deleted file mode 100644 index a883faa693c1f..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts index ac13b7dc21293..ec26773fc3bf9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const table: ElementFactory = () => ({ name: 'table', displayName: 'Data table', - tags: ['text'], + type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', - image: header, + icon: 'visTable', expression: `filters | demodata | table diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png deleted file mode 100644 index b3329f991158c..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts deleted file mode 100644 index 21d8ba1e1b04a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts +++ /dev/null @@ -1,23 +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 { ElementFactory } from '../../../types'; -import header from './header.png'; - -export const tiltedPie: ElementFactory = () => ({ - name: 'tiltedPie', - displayName: 'Tilted pie chart', - tags: ['chart', 'proportion'], - width: 500, - height: 250, - help: 'A customizable tilted pie chart', - image: header, - expression: `filters -| demodata -| pointseries color="project" size="max(price)" -| pie tilt=0.5 -| render`, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png deleted file mode 100644 index d36b4cc97e5b1..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts index b384707c243d1..702ccb8a2312f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const timeFilter: ElementFactory = () => ({ - name: 'time_filter', + name: 'timeFilter', displayName: 'Time filter', - tags: ['filter'], + type: 'filter', help: 'Set a time window', - image: header, + icon: 'calendar', height: 50, expression: `timefilterControl compact=true column=@timestamp | render`, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png deleted file mode 100644 index 90505dd0dc77d..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts index ac4e3a0a72150..2354be0328e7c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalBarChart: ElementFactory = () => ({ name: 'verticalBarChart', displayName: 'Vertical bar chart', - tags: ['chart'], + type: 'chart', help: 'A customizable vertical bar chart', - image: header, + icon: 'visBarVertical', expression: `filters | demodata | pointseries x="project" y="size(cost)" color="project" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png deleted file mode 100644 index b9ff963e92c31..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts index e12903dafede9..b5e6c9816e3f5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalProgressBar: ElementFactory = () => ({ name: 'verticalProgressBar', displayName: 'Vertical progress bar', - tags: ['chart', 'proportion'], + type: 'progress', help: 'Displays progress as a portion of a vertical bar', width: 80, height: 400, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png deleted file mode 100644 index a4ac6b57da236..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts index 8926a12da8a47..28e80372494db 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalProgressPill: ElementFactory = () => ({ name: 'verticalProgressPill', displayName: 'Vertical progress pill', - tags: ['chart', 'proportion'], + type: 'progress', help: 'Displays progress as a portion of a vertical pill', width: 80, height: 400, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts index 2f7f5c26ad0b7..61ecd66455e78 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts @@ -8,7 +8,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Render } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; -interface Arguments { +export interface Arguments { column: string; compact: boolean; filterGroup: string; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts index a654c6b28b350..4452e5e9e31fe 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts @@ -14,18 +14,16 @@ import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; // @ts-ignore: untyped local import { renderFunctions, renderFunctionFactories } from './renderers'; - -import { elementSpecs } from './elements'; +import { initializeElements } from './elements'; // @ts-ignore Untyped Local import { transformSpecs } from './uis/transforms'; // @ts-ignore Untyped Local import { datasourceSpecs } from './uis/datasources'; // @ts-ignore Untyped Local import { modelSpecs } from './uis/models'; +import { initializeViews } from './uis/views'; // @ts-ignore Untyped Local -import { viewSpecs } from './uis/views'; -// @ts-ignore Untyped Local -import { args as argSpecs } from './uis/arguments'; +import { initializeArgs } from './uis/arguments'; import { tagSpecs } from './uis/tags'; import { templateSpecs } from './templates'; @@ -39,6 +37,9 @@ export interface StartDeps { inspector: InspectorStart; } +export type SetupInitializer = (core: CoreSetup, plugins: SetupDeps) => T; +export type StartInitializer = (core: CoreStart, plugins: StartDeps) => T; + /** @internal */ export class CanvasSrcPlugin implements Plugin { public setup(core: CoreSetup, plugins: SetupDeps) { @@ -53,11 +54,11 @@ export class CanvasSrcPlugin implements Plugin ); }); - plugins.canvas.addElements(elementSpecs); + plugins.canvas.addElements(initializeElements(core, plugins)); plugins.canvas.addDatasourceUIs(datasourceSpecs); plugins.canvas.addModelUIs(modelSpecs); - plugins.canvas.addViewUIs(viewSpecs); - plugins.canvas.addArgumentUIs(argSpecs); + plugins.canvas.addViewUIs(initializeViews(core, plugins)); + plugins.canvas.addArgumentUIs(initializeArgs(core, plugins)); plugins.canvas.addTagUIs(tagSpecs); plugins.canvas.addTemplates(templateSpecs); plugins.canvas.addTransformUIs(transformSpecs); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index a1096d50c1653..ee08dfb87e1c1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -6,7 +6,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { I18nContext } from 'ui/i18n'; import { CoreStart } from '../../../../../../../src/core/public'; import { StartDeps } from '../../plugin'; import { @@ -30,6 +29,8 @@ const embeddablesRegistry: { } = {}; const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { + const I18nContext = core.i18n.Context; + return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { return (
{ - const customQuickRanges = (getAdvancedSettings().get('timepicker:quickRanges') || []).map( - ({ from, to, display }: { from: string; to: string; display: string }) => ({ - start: from, - end: to, - label: display, - }) - ); - - const customDateFormat = getAdvancedSettings().get('dateFormat'); - - return ( - - ); -}; +export { TimeFilter }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js deleted file mode 100644 index cbc514e218d74..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js +++ /dev/null @@ -1,47 +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 ReactDOM from 'react-dom'; -import React from 'react'; -import { toExpression } from '@kbn/interpreter/common'; -import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; -import { RendererStrings } from '../../../i18n'; -import { TimeFilter } from './components'; - -const { timeFilter: strings } = RendererStrings; - -export const timeFilter = () => ({ - name: 'time_filter', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, // must be true, otherwise popovers don't work - render(domNode, config, handlers) { - const filterExpression = handlers.getFilter(); - - if (filterExpression !== '') { - // NOTE: setFilter() will cause a data refresh, avoid calling unless required - // compare expression and filter, update filter if needed - const { changed, newAst } = syncFilterExpression(config, filterExpression, [ - 'column', - 'filterGroup', - ]); - - if (changed) { - handlers.setFilter(toExpression(newAst)); - } - } - - ReactDOM.render( - , - domNode, - () => handlers.done() - ); - - handlers.onDestroy(() => { - ReactDOM.unmountComponentAtNode(domNode); - }); - }, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx new file mode 100644 index 0000000000000..ef5bfb70d4b3d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx @@ -0,0 +1,70 @@ +/* + * 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 ReactDOM from 'react-dom'; +import React from 'react'; +import { toExpression } from '@kbn/interpreter/common'; +import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; +import { RendererStrings } from '../../../i18n'; +import { TimeFilter } from './components'; +import { StartInitializer } from '../../plugin'; +import { RendererHandlers } from '../../../types'; +import { Arguments } from '../../functions/common/timefilterControl'; +import { RendererFactory } from '../../../types'; + +const { timeFilter: strings } = RendererStrings; + +export const timeFilterFactory: StartInitializer> = (core, plugins) => { + const { uiSettings } = core; + + const customQuickRanges = (uiSettings.get('timepicker:quickRanges') || []).map( + ({ from, to, display }: { from: string; to: string; display: string }) => ({ + start: from, + end: to, + label: display, + }) + ); + + const customDateFormat = uiSettings.get('dateFormat'); + + return () => ({ + name: 'time_filter', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, // must be true, otherwise popovers don't work + render: async (domNode: HTMLElement, config: Arguments, handlers: RendererHandlers) => { + const filterExpression = handlers.getFilter(); + + if (filterExpression !== '') { + // NOTE: setFilter() will cause a data refresh, avoid calling unless required + // compare expression and filter, update filter if needed + const { changed, newAst } = syncFilterExpression(config, filterExpression, [ + 'column', + 'filterGroup', + ]); + + if (changed) { + handlers.setFilter(toExpression(newAst)); + } + } + + ReactDOM.render( + , + domNode, + () => handlers.done() + ); + + handlers.onDestroy(() => { + ReactDOM.unmountComponentAtNode(domNode); + }); + }, + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts index d19bfa64bae76..655a362fe6d33 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts @@ -7,38 +7,40 @@ import { compose, withProps } from 'recompose'; import moment from 'moment'; import { DateFormatArgInput as Component, Props as ComponentProps } from './date_format'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; -const { DateFormat: strings } = ArgumentStrings; +import { SetupInitializer } from '../../../plugin'; -const getFormatMap = () => ({ - DEFAULT: getAdvancedSettings().get('dateFormat'), - NANOS: getAdvancedSettings().get('dateNanosFormat'), - ISO8601: '', - LOCAL_LONG: 'LLLL', - LOCAL_SHORT: 'LLL', - LOCAL_DATE: 'l', - LOCAL_TIME_WITH_SECONDS: 'LTS', -}); +const { DateFormat: strings } = ArgumentStrings; -const now = moment(); +export const dateFormatInitializer: SetupInitializer> = ( + core, + plugins +) => { + const formatMap = { + DEFAULT: core.uiSettings.get('dateFormat'), + NANOS: core.uiSettings.get('dateNanosFormat'), + ISO8601: '', + LOCAL_LONG: 'LLLL', + LOCAL_SHORT: 'LLL', + LOCAL_DATE: 'l', + LOCAL_TIME_WITH_SECONDS: 'LTS', + }; -const dateFormats = Object.values(getFormatMap()).map(format => ({ - value: format, - text: moment.utc(now).format(format), -})); + const dateFormats = Object.values(formatMap).map(format => ({ + value: format, + text: moment.utc(moment()).format(format), + })); -export const DateFormatArgInput = compose(withProps({ dateFormats }))( - Component -); + const DateFormatArgInput = compose(withProps({ dateFormats }))(Component); -export const dateFormat: ArgumentFactory = () => ({ - name: 'dateFormat', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(DateFormatArgInput), -}); + return () => ({ + name: 'dateFormat', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(DateFormatArgInput), + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js deleted file mode 100644 index 829fe3e3bef3d..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js +++ /dev/null @@ -1,39 +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 { axisConfig } from './axis_config'; -import { datacolumn } from './datacolumn'; -import { dateFormat } from './date_format'; -import { filterGroup } from './filter_group'; -import { imageUpload } from './image_upload'; -import { number } from './number'; -import { numberFormat } from './number_format'; -import { palette } from './palette'; -import { percentage } from './percentage'; -import { range } from './range'; -import { select } from './select'; -import { shape } from './shape'; -import { string } from './string'; -import { textarea } from './textarea'; -import { toggle } from './toggle'; - -export const args = [ - axisConfig, - datacolumn, - dateFormat, - filterGroup, - imageUpload, - number, - numberFormat, - palette, - percentage, - range, - select, - shape, - string, - textarea, - toggle, -]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts new file mode 100644 index 0000000000000..bd84a2eb97d24 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { axisConfig } from './axis_config'; +// @ts-ignore untyped local +import { datacolumn } from './datacolumn'; +import { dateFormatInitializer } from './date_format'; +// @ts-ignore untyped local +import { filterGroup } from './filter_group'; +// @ts-ignore untyped local +import { imageUpload } from './image_upload'; +// @ts-ignore untyped local +import { number } from './number'; +import { numberFormatInitializer } from './number_format'; +// @ts-ignore untyped local +import { palette } from './palette'; +// @ts-ignore untyped local +import { percentage } from './percentage'; +// @ts-ignore untyped local +import { range } from './range'; +// @ts-ignore untyped local +import { select } from './select'; +// @ts-ignore untyped local +import { shape } from './shape'; +// @ts-ignore untyped local +import { string } from './string'; +// @ts-ignore untyped local +import { textarea } from './textarea'; +// @ts-ignore untyped local +import { toggle } from './toggle'; + +import { SetupInitializer } from '../../plugin'; + +export const args = [ + axisConfig, + datacolumn, + filterGroup, + imageUpload, + number, + palette, + percentage, + range, + select, + shape, + string, + textarea, + toggle, +]; + +export const initializers = [dateFormatInitializer, numberFormatInitializer]; + +export const initializeArgs: SetupInitializer = (core, plugins) => { + return [...args, ...initializers.map(initializer => initializer(core, plugins))]; +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts index ce6c90c89a5a0..4025d4deaf997 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -6,40 +6,42 @@ import { compose, withProps } from 'recompose'; import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; +import { SetupInitializer } from '../../../plugin'; const { NumberFormat: strings } = ArgumentStrings; -const getFormatMap = () => ({ - NUMBER: getAdvancedSettings().get('format:number:defaultPattern'), - PERCENT: getAdvancedSettings().get('format:percent:defaultPattern'), - CURRENCY: getAdvancedSettings().get('format:currency:defaultPattern'), - DURATION: '00:00:00', - BYTES: getAdvancedSettings().get('format:bytes:defaultPattern'), -}); +export const numberFormatInitializer: SetupInitializer> = ( + core, + plugins +) => { + const formatMap = { + NUMBER: core.uiSettings.get('format:number:defaultPattern'), + PERCENT: core.uiSettings.get('format:percent:defaultPattern'), + CURRENCY: core.uiSettings.get('format:currency:defaultPattern'), + DURATION: '00:00:00', + BYTES: core.uiSettings.get('format:bytes:defaultPattern'), + }; -const getNumberFormats = () => { - const formatMap = getFormatMap(); - return [ + const numberFormats = [ { value: formatMap.NUMBER, text: strings.getFormatNumber() }, { value: formatMap.PERCENT, text: strings.getFormatPercent() }, { value: formatMap.CURRENCY, text: strings.getFormatCurrency() }, { value: formatMap.DURATION, text: strings.getFormatDuration() }, { value: formatMap.BYTES, text: strings.getFormatBytes() }, ]; -}; -export const NumberFormatArgInput = compose( - withProps({ numberFormats: getNumberFormats() }) -)(Component); + const NumberFormatArgInput = compose(withProps({ numberFormats }))( + Component + ); -export const numberFormat: ArgumentFactory = () => ({ - name: 'numberFormat', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(NumberFormatArgInput), -}); + return () => ({ + name: 'numberFormat', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(NumberFormatArgInput), + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts deleted file mode 100644 index 4c535a42c3c44..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts +++ /dev/null @@ -1,15 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const chart: TagFactory = () => ({ - name: strings.chart(), - color: euiVisPalette[4], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts deleted file mode 100644 index 5249856dec271..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts +++ /dev/null @@ -1,16 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; - -const euiVisPalette = euiPaletteColorBlind(); - -export const filter: TagFactory = () => ({ - name: strings.filter(), - color: euiVisPalette[1], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts deleted file mode 100644 index 36d66801ef681..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts +++ /dev/null @@ -1,15 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const graphic: TagFactory = () => ({ - name: strings.graphic(), - color: euiVisPalette[5], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts index 2587665a452b5..2e711437c72a8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { chart } from './chart'; -import { filter } from './filter'; -import { graphic } from './graphic'; import { presentation } from './presentation'; -import { proportion } from './proportion'; import { report } from './report'; -import { text } from './text'; // Registry expects a function that returns a spec object -export const tagSpecs = [chart, filter, graphic, presentation, proportion, report, text]; +export const tagSpecs = [presentation, report]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js deleted file mode 100644 index 6a59a6795d45a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js +++ /dev/null @@ -1,10 +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 { euiPaletteColorBlind } from '@elastic/eui'; -const euiVisPalette = euiPaletteColorBlind(); - -export const proportion = () => ({ name: 'proportion', color: euiVisPalette[3] }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts deleted file mode 100644 index 4d37ecfaa367a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts +++ /dev/null @@ -1,15 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const proportion: TagFactory = () => ({ - name: strings.proportion(), - color: euiVisPalette[3], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts deleted file mode 100644 index f9faf2ad2e3ca..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts +++ /dev/null @@ -1,13 +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 { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; - -export const text: TagFactory = () => ({ - name: strings.text(), - color: '#D3DAE6', -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js deleted file mode 100644 index 6e2685dcb9893..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js +++ /dev/null @@ -1,37 +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 { dropdownControl } from './dropdownControl'; -import { getCell } from './getCell'; -import { image } from './image'; -import { markdown } from './markdown'; -import { metric } from './metric'; -import { pie } from './pie'; -import { plot } from './plot'; -import { progress } from './progress'; -import { repeatImage } from './repeatImage'; -import { revealImage } from './revealImage'; -import { render } from './render'; -import { shape } from './shape'; -import { table } from './table'; -import { timefilterControl } from './timefilterControl'; - -export const viewSpecs = [ - dropdownControl, - getCell, - image, - markdown, - metric, - pie, - plot, - progress, - repeatImage, - revealImage, - render, - shape, - table, - timefilterControl, -]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts new file mode 100644 index 0000000000000..fdcaf21982050 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +// @ts-ignore untyped local +import { dropdownControl } from './dropdownControl'; +// @ts-ignore untyped local +import { getCell } from './getCell'; +// @ts-ignore untyped local +import { image } from './image'; +// @ts-ignore untyped local +import { markdown } from './markdown'; +// @ts-ignore untyped local +import { metricInitializer } from './metric'; +// @ts-ignore untyped local +import { pie } from './pie'; +// @ts-ignore untyped local +import { plot } from './plot'; +// @ts-ignore untyped local +import { progress } from './progress'; +// @ts-ignore untyped local +import { repeatImage } from './repeatImage'; +// @ts-ignore untyped local +import { revealImage } from './revealImage'; +// @ts-ignore untyped local +import { render } from './render'; +// @ts-ignore untyped local +import { shape } from './shape'; +// @ts-ignore untyped local +import { table } from './table'; +// @ts-ignore untyped local +import { timefilterControl } from './timefilterControl'; + +import { SetupInitializer } from '../../plugin'; + +export const viewSpecs = [ + dropdownControl, + getCell, + image, + markdown, + pie, + plot, + progress, + repeatImage, + revealImage, + render, + shape, + table, + timefilterControl, +]; + +export const viewInitializers = [metricInitializer]; + +export const initializeViews: SetupInitializer = (core, plugins) => { + return [...viewSpecs, ...viewInitializers.map(initializer => initializer(core, plugins))]; +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js deleted file mode 100644 index e69f8f1de5952..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ /dev/null @@ -1,48 +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 { openSans } from '../../../common/lib/fonts'; -import { getAdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; -import { ViewStrings } from '../../../i18n'; - -const { Metric: strings } = ViewStrings; - -export const metric = () => ({ - name: 'metric', - displayName: strings.getDisplayName(), - modelArgs: [['_', { label: strings.getNumberDisplayName() }]], - requiresContext: false, - args: [ - { - name: 'metricFormat', - displayName: strings.getMetricFormatDisplayName(), - help: strings.getMetricFormatHelp(), - argType: 'numberFormat', - default: `"${getAdvancedSettings().get('format:number:defaultPattern')}"`, - }, - { - name: '_', - displayName: strings.getLabelDisplayName(), - help: strings.getLabelHelp(), - argType: 'string', - default: '""', - }, - { - name: 'metricFont', - displayName: strings.getMetricFontDisplayName(), - help: strings.getMetricFontHelp(), - argType: 'font', - default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, - }, - { - name: 'labelFont', - displayName: strings.getLabelFontDisplayName(), - help: strings.getLabelFontHelp(), - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, - }, - ], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts new file mode 100644 index 0000000000000..93912b7b0517f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts @@ -0,0 +1,50 @@ +/* + * 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 { openSans } from '../../../common/lib/fonts'; +import { ViewStrings } from '../../../i18n'; +import { SetupInitializer } from '../../plugin'; + +const { Metric: strings } = ViewStrings; + +export const metricInitializer: SetupInitializer = (core, plugin) => { + return () => ({ + name: 'metric', + displayName: strings.getDisplayName(), + modelArgs: [['_', { label: strings.getNumberDisplayName() }]], + requiresContext: false, + args: [ + { + name: 'metricFormat', + displayName: strings.getMetricFormatDisplayName(), + help: strings.getMetricFormatHelp(), + argType: 'numberFormat', + default: `"${core.uiSettings.get('format:number:defaultPattern')}"`, + }, + { + name: '_', + displayName: strings.getLabelDisplayName(), + help: strings.getLabelHelp(), + argType: 'string', + default: '""', + }, + { + name: 'metricFont', + displayName: strings.getMetricFontDisplayName(), + help: strings.getMetricFontHelp(), + argType: 'font', + default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, + }, + { + name: 'labelFont', + displayName: strings.getLabelFontDisplayName(), + help: strings.getLabelFontHelp(), + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, + }, + ], + }); +}; diff --git a/x-pack/legacy/plugins/canvas/common/lib/constants.ts b/x-pack/legacy/plugins/canvas/common/lib/constants.ts index ac8e80b8d7b89..a37dc3fd6a7b3 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/constants.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/constants.ts @@ -40,3 +40,4 @@ export const API_ROUTE_SHAREABLE_ZIP = '/public/canvas/zip'; export const API_ROUTE_SHAREABLE_RUNTIME = '/public/canvas/runtime'; export const API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD = `/public/canvas/${SHAREABLE_RUNTIME_NAME}.js`; export const CANVAS_EMBEDDABLE_CLASSNAME = `canvasEmbeddable`; +export const CONTEXT_MENU_TOP_BORDER_CLASSNAME = 'canvasContextMenu--topBorder'; diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index d0a9051d7af87..7bd16c4933ce1 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -15,7 +15,7 @@ export const ComponentStrings = { }), getTitleText: () => i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Embed Object', + defaultMessage: 'Add from Visualize library', }), }, AdvancedFilter: { @@ -305,21 +305,21 @@ export const ComponentStrings = { }), }, ElementControls: { - getEditTooltip: () => - i18n.translate('xpack.canvas.elementControls.editToolTip', { - defaultMessage: 'Edit', - }), - getEditAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.editAriaLabel', { - defaultMessage: 'Edit element', + getDeleteAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { + defaultMessage: 'Delete element', }), getDeleteTooltip: () => i18n.translate('xpack.canvas.elementControls.deleteToolTip', { defaultMessage: 'Delete', }), - getDeleteAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { - defaultMessage: 'Delete element', + getEditAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.editAriaLabel', { + defaultMessage: 'Edit element', + }), + getEditTooltip: () => + i18n.translate('xpack.canvas.elementControls.editToolTip', { + defaultMessage: 'Edit', }), }, ElementSettings: { @@ -336,53 +336,6 @@ export const ComponentStrings = { description: 'This tab contains the settings for how data is displayed in a Canvas element', }), }, - ElementTypes: { - getEditElementTitle: () => - i18n.translate('xpack.canvas.elementTypes.editElementTitle', { - defaultMessage: 'Edit element', - }), - getDeleteElementTitle: (elementName: string) => - i18n.translate('xpack.canvas.elementTypes.deleteElementTitle', { - defaultMessage: `Delete element '{elementName}'?`, - values: { - elementName, - }, - }), - getDeleteElementDescription: () => - i18n.translate('xpack.canvas.elementTypes.deleteElementDescription', { - defaultMessage: 'Are you sure you want to delete this element?', - }), - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.elementTypes.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getDeleteButtonLabel: () => - i18n.translate('xpack.canvas.elementTypes.deleteButtonLabel', { - defaultMessage: 'Delete', - }), - getAddNewElementTitle: () => - i18n.translate('xpack.canvas.elementTypes.addNewElementTitle', { - defaultMessage: 'Add new elements', - }), - getAddNewElementDescription: () => - i18n.translate('xpack.canvas.elementTypes.addNewElementDescription', { - defaultMessage: 'Group and save workpad elements to create new elements', - }), - getFindElementPlaceholder: () => - i18n.translate('xpack.canvas.elementTypes.findElementPlaceholder', { - defaultMessage: 'Find element', - }), - getElementsTitle: () => - i18n.translate('xpack.canvas.elementTypes.elementsTitle', { - defaultMessage: 'Elements', - description: 'Title for the "Elements" tab when adding a new element', - }), - getMyElementsTitle: () => - i18n.translate('xpack.canvas.elementTypes.myElementsTitle', { - defaultMessage: 'My elements', - description: 'Title for the "My elements" tab when adding a new element', - }), - }, Error: { getDescription: () => i18n.translate('xpack.canvas.errorComponent.description', { @@ -633,6 +586,61 @@ export const ComponentStrings = { defaultMessage: 'Delete', }), }, + SavedElementsModal: { + getAddNewElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementDescription', { + defaultMessage: 'Group and save workpad elements to create new elements', + }), + getAddNewElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementTitle', { + defaultMessage: 'Add new elements', + }), + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getDeleteButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + getDeleteElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementDescription', { + defaultMessage: 'Are you sure you want to delete this element?', + }), + getDeleteElementTitle: (elementName: string) => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementTitle', { + defaultMessage: `Delete element '{elementName}'?`, + values: { + elementName, + }, + }), + getEditElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.editElementTitle', { + defaultMessage: 'Edit element', + }), + getElementsTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.elementsTitle', { + defaultMessage: 'Elements', + description: 'Title for the "Elements" tab when adding a new element', + }), + getFindElementPlaceholder: () => + i18n.translate('xpack.canvas.savedElementsModal.findElementPlaceholder', { + defaultMessage: 'Find element', + }), + getModalTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.modalTitle', { + defaultMessage: 'My elements', + }), + getMyElementsTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.myElementsTitle', { + defaultMessage: 'My elements', + description: 'Title for the "My elements" tab when adding a new element', + }), + getSavedElementsModalCloseButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { + defaultMessage: 'Close', + }), + }, ShareWebsiteFlyout: { getRuntimeStepTitle: () => i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', { @@ -652,7 +660,7 @@ export const ComponentStrings = { defaultMessage: 'Share on a website', }), getUnsupportedRendererWarning: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning', { defaultMessage: 'This workpad contains render functions that are not supported by the {CANVAS} Shareable Workpad Runtime. These elements will not be rendered:', values: { @@ -900,6 +908,10 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.alignRightOption', { defaultMessage: 'Align right', }), + getFontColorLabel: () => + i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { + defaultMessage: 'Font Color', + }), getStyleBoldOption: () => i18n.translate('xpack.canvas.textStylePicker.styleBoldOption', { defaultMessage: 'Bold', @@ -912,10 +924,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', { defaultMessage: 'Underline', }), - getFontColorLabel: () => - i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { - defaultMessage: 'Font Color', - }), }, TimePicker: { getApplyButtonLabel: () => @@ -962,6 +970,10 @@ export const ComponentStrings = { description: '"stylesheet" refers to the collection of CSS style rules entered by the user.', }), + getBackgroundColorLabel: () => + i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { + defaultMessage: 'Background color', + }), getFlipDimensionAriaLabel: () => i18n.translate('xpack.canvas.workpadConfig.swapDimensionsAriaLabel', { defaultMessage: `Swap the page's width and height`, @@ -1013,10 +1025,6 @@ export const ComponentStrings = { defaultMessage: 'US Letter', description: 'This is referring to the dimensions of U.S. standard letter paper.', }), - getBackgroundColorLabel: () => - i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { - defaultMessage: 'Background color', - }), }, WorkpadCreate: { getWorkpadCreateButtonLabel: () => @@ -1029,14 +1037,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', { defaultMessage: 'Add element', }), - getAddElementModalCloseButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { - defaultMessage: 'Close', - }), - getEmbedObjectButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.embedObjectButtonLabel', { - defaultMessage: 'Embed object', - }), getFullScreenButtonAriaLabel: () => i18n.translate('xpack.canvas.workpadHeader.fullscreenButtonAriaLabel', { defaultMessage: 'View fullscreen', @@ -1080,9 +1080,9 @@ export const ComponentStrings = { }), }, WorkpadHeaderControlSettings: { - getTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderControlSettings.settingsTooltip', { - defaultMessage: 'Control settings', + getButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderControlSettings.buttonLabel', { + defaultMessage: 'Options', }), }, WorkpadHeaderCustomInterval: { @@ -1105,6 +1105,56 @@ export const ComponentStrings = { defaultMessage: 'Set a custom interval', }), }, + WorkpadHeaderElementMenu: { + getAssetsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel', { + defaultMessage: 'Manage assets', + }), + getChartMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.chartMenuItemLabel', { + defaultMessage: 'Chart', + }), + getElementMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuButtonLabel', { + defaultMessage: 'Add element', + }), + getElementMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuLabel', { + defaultMessage: 'Add an element', + }), + getEmbedObjectMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { + defaultMessage: 'Add from Visualize library', + }), + getFilterMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { + defaultMessage: 'Filter', + }), + getImageMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.imageMenuItemLabel', { + defaultMessage: 'Image', + }), + getMyElementsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.myElementsMenuItemLabel', { + defaultMessage: 'My elements', + }), + getOtherMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.otherMenuItemLabel', { + defaultMessage: 'Other', + }), + getProgressMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.progressMenuItemLabel', { + defaultMessage: 'Progress', + }), + getShapeMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.shapeMenuItemLabel', { + defaultMessage: 'Shape', + }), + getTextMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.textMenuItemLabel', { + defaultMessage: 'Text', + }), + }, WorkpadHeaderKioskControls: { getCycleFormLabel: () => i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleFormLabel', { @@ -1129,9 +1179,9 @@ export const ComponentStrings = { defaultMessage: 'Refresh data', }), }, - WorkpadHeaderWorkpadExport: { + WorkpadHeaderShareMenu: { getCopyPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyPDFMessage', { defaultMessage: 'The {PDF} generation {URL} was copied to your clipboard.', values: { PDF, @@ -1139,15 +1189,15 @@ export const ComponentStrings = { }, }), getCopyReportingConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage', { defaultMessage: 'Copied reporting configuration to clipboard', }), getCopyShareConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { defaultMessage: 'Copied share markup to clipboard', }), getExportPDFErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage', { defaultMessage: "Failed to create {PDF} for '{workpadName}'", values: { PDF, @@ -1155,14 +1205,14 @@ export const ComponentStrings = { }, }), getExportPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFMessage', { defaultMessage: 'Exporting {PDF}. You can track the progress in Management.', values: { PDF, }, }), getExportPDFTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFTitle', { defaultMessage: "{PDF} export of workpad '{workpadName}'", values: { PDF, @@ -1170,7 +1220,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel', { defaultMessage: 'Alternatively, you can generate a {PDF} from a script or with Watcher by using this {URL}. Press Enter to copy the {URL} to clipboard.', values: { @@ -1179,7 +1229,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel', { defaultMessage: 'Copy {POST} {URL}', values: { POST, @@ -1187,7 +1237,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyDescription: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription', { defaultMessage: 'Alternatively, copy this {POST} {URL} to call generation from outside {KIBANA} or from Watcher.', values: { @@ -1197,14 +1247,14 @@ export const ComponentStrings = { }, }), getPDFPanelGenerateButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel', { defaultMessage: 'Generate {PDF}', values: { PDF, }, }), getPDFPanelGenerateDescription: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription', { defaultMessage: '{PDF}s can take a minute or two to generate based on the size of your workpad.', values: { @@ -1212,7 +1262,7 @@ export const ComponentStrings = { }, }), getShareableZipErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { defaultMessage: "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", values: { @@ -1221,69 +1271,101 @@ export const ComponentStrings = { }, }), getShareDownloadJSONTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle', { defaultMessage: 'Download as {JSON}', values: { JSON, }, }), getShareDownloadPDFTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle', { defaultMessage: '{PDF} reports', values: { PDF, }, }), + getShareMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareMenuButtonLabel', { + defaultMessage: 'Share', + }), getShareWebsiteTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle', { defaultMessage: 'Share on a website', }), getShareWorkpadMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage', { defaultMessage: 'Share this workpad', }), getUnknownExportErrorMessage: (type: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { defaultMessage: 'Unknown export type: {type}', values: { type, }, }), }, - WorkpadHeaderWorkpadZoom: { + WorkpadHeaderViewMenu: { + getFullscreenMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel', { + defaultMessage: 'Enter fullscreen mode', + }), + getHideEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.hideEditModeLabel', { + defaultMessage: 'Hide editing controls', + }), + getRefreshMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshMenuItemLabel', { + defaultMessage: 'Refresh data', + }), + getShowEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.showEditModeLabel', { + defaultMessage: 'Show editing controls', + }), + getViewMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuButtonLabel', { + defaultMessage: 'View', + }), + getViewMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuLabel', { + defaultMessage: 'View options', + }), getZoomControlsAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel', { defaultMessage: 'Zoom controls', }), getZoomControlsTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip', { defaultMessage: 'Zoom controls', }), getZoomFitToWindowText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText', { defaultMessage: 'Fit to window', }), getZoomInText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomInText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomInText', { defaultMessage: 'Zoom in', }), + getZoomMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel', { + defaultMessage: 'Zoom', + }), getZoomOutText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomOutText', { defaultMessage: 'Zoom out', }), getZoomPanelTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle', { defaultMessage: 'Zoom', }), getZoomPercentage: (scale: number) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomResetText', { defaultMessage: '{scalePercentage}%', values: { scalePercentage: scale * 100, }, }), getZoomResetText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue', { defaultMessage: 'Reset', }), }, diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts index 41f88a3c75f90..4464ed5dbf185 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts @@ -7,8 +7,6 @@ import { ElementFactory } from '../../types'; import { getElementStrings } from './index'; -import { TagStrings } from '../../i18n'; - /** * This function takes a set of Canvas Element specification factories, runs them, * replaces relevant strings (if available) and returns a new factory. We do this @@ -34,17 +32,6 @@ export const applyElementStrings = (elements: ElementFactory[]) => { if (displayName) { result.displayName = displayName; } - - // Set translated tags - if (result.tags) { - result.tags = result.tags.map(tag => { - if (tag in TagStrings) { - return TagStrings[tag](); - } - - return tag; - }); - } } return () => result; diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts index 3d835bdf31bf8..c28229bdab33f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); import { getElementStrings } from './element_strings'; -import { elementSpecs } from '../../canvas_plugin_src/elements'; +import { initializeElements } from '../../canvas_plugin_src/elements'; +import { coreMock } from '../../../../../../src/core/public/mocks'; -import { TagStrings } from '../tags'; +const elementSpecs = initializeElements(coreMock.createSetup() as any, {} as any); describe('ElementStrings', () => { const elementStrings = getElementStrings(); @@ -35,15 +35,4 @@ describe('ElementStrings', () => { expect(value).toHaveProperty('help'); }); }); - - test('All elements should have tags that are defined', () => { - const tagNames = Object.keys(TagStrings); - - elementSpecs.forEach(spec => { - const element = spec(); - if (element.tags) { - element.tags.forEach((tagName: string) => expect(tagNames).toContain(tagName)); - } - }); - }); }); diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts index df23dff1c7127..595ef4cb92206 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts @@ -23,7 +23,7 @@ interface ElementStringDict { export const getElementStrings = (): ElementStringDict => ({ areaChart: { displayName: i18n.translate('xpack.canvas.elements.areaChartDisplayName', { - defaultMessage: 'Area chart', + defaultMessage: 'Area', }), help: i18n.translate('xpack.canvas.elements.areaChartHelpText', { defaultMessage: 'A line chart with a filled body', @@ -31,7 +31,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, bubbleChart: { displayName: i18n.translate('xpack.canvas.elements.bubbleChartDisplayName', { - defaultMessage: 'Bubble chart', + defaultMessage: 'Bubble', }), help: i18n.translate('xpack.canvas.elements.bubbleChartHelpText', { defaultMessage: 'A customizable bubble chart', @@ -39,31 +39,31 @@ export const getElementStrings = (): ElementStringDict => ({ }, debug: { displayName: i18n.translate('xpack.canvas.elements.debugDisplayName', { - defaultMessage: 'Debug', + defaultMessage: 'Debug data', }), help: i18n.translate('xpack.canvas.elements.debugHelpText', { defaultMessage: 'Just dumps the configuration of the element', }), }, - donut: { - displayName: i18n.translate('xpack.canvas.elements.donutChartDisplayName', { - defaultMessage: 'Donut chart', - }), - help: i18n.translate('xpack.canvas.elements.donutChartHelpText', { - defaultMessage: 'A customizable donut chart', - }), - }, - dropdown_filter: { + dropdownFilter: { displayName: i18n.translate('xpack.canvas.elements.dropdownFilterDisplayName', { - defaultMessage: 'Dropdown filter', + defaultMessage: 'Dropdown select', }), help: i18n.translate('xpack.canvas.elements.dropdownFilterHelpText', { defaultMessage: 'A dropdown from which you can select values for an "exactly" filter', }), }, + filterDebug: { + displayName: i18n.translate('xpack.canvas.elements.filterDebugDisplayName', { + defaultMessage: 'Debug filters', + }), + help: i18n.translate('xpack.canvas.elements.filterDebugHelpText', { + defaultMessage: 'Shows the underlying global filters in a workpad', + }), + }, horizontalBarChart: { displayName: i18n.translate('xpack.canvas.elements.horizontalBarChartDisplayName', { - defaultMessage: 'Horizontal bar chart', + defaultMessage: 'Horizontal bar', }), help: i18n.translate('xpack.canvas.elements.horizontalBarChartHelpText', { defaultMessage: 'A customizable horizontal bar chart', @@ -71,7 +71,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, horizontalProgressBar: { displayName: i18n.translate('xpack.canvas.elements.horizontalProgressBarDisplayName', { - defaultMessage: 'Horizontal progress bar', + defaultMessage: 'Horizontal bar', }), help: i18n.translate('xpack.canvas.elements.horizontalProgressBarHelpText', { defaultMessage: 'Displays progress as a portion of a horizontal bar', @@ -79,7 +79,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, horizontalProgressPill: { displayName: i18n.translate('xpack.canvas.elements.horizontalProgressPillDisplayName', { - defaultMessage: 'Horizontal progress pill', + defaultMessage: 'Horizontal pill', }), help: i18n.translate('xpack.canvas.elements.horizontalProgressPillHelpText', { defaultMessage: 'Displays progress as a portion of a horizontal pill', @@ -95,7 +95,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, lineChart: { displayName: i18n.translate('xpack.canvas.elements.lineChartDisplayName', { - defaultMessage: 'Line chart', + defaultMessage: 'Line', }), help: i18n.translate('xpack.canvas.elements.lineChartHelpText', { defaultMessage: 'A customizable line chart', @@ -119,7 +119,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, pie: { displayName: i18n.translate('xpack.canvas.elements.pieDisplayName', { - defaultMessage: 'Pie chart', + defaultMessage: 'Pie', }), help: i18n.translate('xpack.canvas.elements.pieHelpText', { defaultMessage: 'Pie chart', @@ -135,7 +135,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressGauge: { displayName: i18n.translate('xpack.canvas.elements.progressGaugeDisplayName', { - defaultMessage: 'Progress gauge', + defaultMessage: 'Gauge', }), help: i18n.translate('xpack.canvas.elements.progressGaugeHelpText', { defaultMessage: 'Displays progress as a portion of a gauge', @@ -143,7 +143,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressSemicircle: { displayName: i18n.translate('xpack.canvas.elements.progressSemicircleDisplayName', { - defaultMessage: 'Progress semicircle', + defaultMessage: 'Semicircle', }), help: i18n.translate('xpack.canvas.elements.progressSemicircleHelpText', { defaultMessage: 'Displays progress as a portion of a semicircle', @@ -151,7 +151,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressWheel: { displayName: i18n.translate('xpack.canvas.elements.progressWheelDisplayName', { - defaultMessage: 'Progress wheel', + defaultMessage: 'Wheel', }), help: i18n.translate('xpack.canvas.elements.progressWheelHelpText', { defaultMessage: 'Displays progress as a portion of a wheel', @@ -189,15 +189,7 @@ export const getElementStrings = (): ElementStringDict => ({ defaultMessage: 'A scrollable grid for displaying data in a tabular format', }), }, - tiltedPie: { - displayName: i18n.translate('xpack.canvas.elements.tiltedPieDisplayName', { - defaultMessage: 'Tilted pie chart', - }), - help: i18n.translate('xpack.canvas.elements.tiltedPieHelpText', { - defaultMessage: 'A customizable tilted pie chart', - }), - }, - time_filter: { + timeFilter: { displayName: i18n.translate('xpack.canvas.elements.timeFilterDisplayName', { defaultMessage: 'Time filter', }), @@ -207,7 +199,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalBarChart: { displayName: i18n.translate('xpack.canvas.elements.verticalBarChartDisplayName', { - defaultMessage: 'Vertical bar chart', + defaultMessage: 'Vertical bar', }), help: i18n.translate('xpack.canvas.elements.verticalBarChartHelpText', { defaultMessage: 'A customizable vertical bar chart', @@ -215,7 +207,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalProgressBar: { displayName: i18n.translate('xpack.canvas.elements.verticalProgressBarDisplayName', { - defaultMessage: 'Vertical progress bar', + defaultMessage: 'Vertical bar', }), help: i18n.translate('xpack.canvas.elements.verticalProgressBarHelpText', { defaultMessage: 'Displays progress as a portion of a vertical bar', @@ -223,7 +215,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalProgressPill: { displayName: i18n.translate('xpack.canvas.elements.verticalProgressPillDisplayName', { - defaultMessage: 'Vertical progress pill', + defaultMessage: 'Vertical pill', }), help: i18n.translate('xpack.canvas.elements.verticalProgressPillHelpText', { defaultMessage: 'Displays progress as a portion of a vertical pill', diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts index 836c7395ac448..cc601b0ea0e31 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { alterColumn } from '../../../canvas_plugin_src/functions/common/alterColumn'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { DATATABLE_COLUMN_TYPES } from '../../../common/lib'; +import { DATATABLE_COLUMN_TYPES } from '../../../common/lib/constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.alterColumnHelpText', { diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts index e105f7e678af3..41bf86055f1e3 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts @@ -5,12 +5,12 @@ */ import { i18n } from '@kbn/i18n'; -import { timelion } from '../../../public/functions/timelion'; +import { timelionFunctionFactory } from '../../../public/functions/timelion'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; import { ELASTICSEARCH, DATEMATH, MOMENTJS_TIMEZONE_URL } from '../../constants'; -export const help: FunctionHelp> = { +export const help: FunctionHelp>> = { help: i18n.translate('xpack.canvas.functions.timelionHelpText', { defaultMessage: 'Use Timelion to extract one or more timeseries from many sources.', }), diff --git a/x-pack/legacy/plugins/canvas/i18n/tags.ts b/x-pack/legacy/plugins/canvas/i18n/tags.ts index 41007c58d738d..9595554260297 100644 --- a/x-pack/legacy/plugins/canvas/i18n/tags.ts +++ b/x-pack/legacy/plugins/canvas/i18n/tags.ts @@ -7,32 +7,12 @@ import { i18n } from '@kbn/i18n'; export const TagStrings: { [key: string]: () => string } = { - chart: () => - i18n.translate('xpack.canvas.tags.chartTag', { - defaultMessage: 'chart', - }), - filter: () => - i18n.translate('xpack.canvas.tags.filterTag', { - defaultMessage: 'filter', - }), - graphic: () => - i18n.translate('xpack.canvas.tags.graphicTag', { - defaultMessage: 'graphic', - }), presentation: () => i18n.translate('xpack.canvas.tags.presentationTag', { defaultMessage: 'presentation', }), - proportion: () => - i18n.translate('xpack.canvas.tags.proportionTag', { - defaultMessage: 'proportion', - }), report: () => i18n.translate('xpack.canvas.tags.reportTag', { defaultMessage: 'report', }), - text: () => - i18n.translate('xpack.canvas.tags.textTag', { - defaultMessage: 'text', - }), }; diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index f75b3b427c41b..f746a24e9b261 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -30,8 +30,12 @@ import { VALUE_CLICK_TRIGGER, ActionByType } from '../../../../../src/plugins/ui /* eslint-disable */ import { ACTION_VALUE_CLICK } from '../../../../../src/plugins/data/public/actions/value_click_action'; /* eslint-enable */ +import { init as initStatsReporter } from './lib/ui_metric'; import { CapabilitiesStrings } from '../i18n'; + +import { startServices, stopServices, services } from './services'; + const { ReadOnlyBadge: strings } = CapabilitiesStrings; let restoreAction: ActionByType | undefined; @@ -50,8 +54,14 @@ export const renderApp = ( { element }: AppMountParameters, canvasStore: Store ) => { + const canvasServices = Object.entries(services).reduce((reduction, [key, provider]) => { + reduction[key] = provider.getService(); + + return reduction; + }, {} as Record); + ReactDOM.render( - + @@ -70,6 +80,8 @@ export const initializeCanvas = async ( startPlugins: CanvasStartDeps, registries: SetupRegistries ) => { + startServices(coreSetup, coreStart, setupPlugins, startPlugins); + // Create Store const canvasStore = await createStore(coreSetup, setupPlugins); @@ -121,10 +133,15 @@ export const initializeCanvas = async ( startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, emptyAction); } + if (setupPlugins.usageCollection) { + initStatsReporter(setupPlugins.usageCollection.reportUiStats); + } + return canvasStore; }; export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => { + stopServices(); destroyRegistries(); resetInterpreter(); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js b/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js index 718443fcdd990..4e3920bf34f67 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js @@ -6,7 +6,7 @@ import { ErrorStrings } from '../../../i18n'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; +import { notifyService } from '../../services'; import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; import { getDefaultWorkpad } from '../../state/defaults'; import { setWorkpad } from '../../state/actions/workpad'; @@ -33,7 +33,9 @@ export const routes = [ dispatch(resetAssets()); router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: strings.getCreateFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getCreateFailureErrorMessage() }); router.redirectTo('home'); } }, @@ -59,7 +61,9 @@ export const routes = [ // reset transient properties when changing workpads dispatch(setZoomScale(1)); } catch (err) { - notify.error(err, { title: strings.getLoadFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getLoadFailureErrorMessage() }); return router.redirectTo('home'); } } diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss index c5161439b71c3..c7dae8452a93c 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: ($euiSizeXS + 1px) $euiSize $euiSizeXS; + padding: 1px $euiSize 0; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot index 5cb64964ee38f..6601f570209e9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot @@ -18,13 +18,13 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` className="canvasAsset__thumb canvasCheckered" >
Asset thumbnail
@@ -192,13 +192,13 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` className="canvasAsset__thumb canvasCheckered" >
Asset thumbnail
diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot new file mode 100644 index 0000000000000..aff630b21c770 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot @@ -0,0 +1,761 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ Manage workpad assets +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+

+ Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets. +

+
+
+
+
+
+
+
+ +

+ Import your assets to get started +

+
+ +
+
+
+
+
+
+
+ +
+
+
+ 0% space used +
+
+
+ +
+
+
+
, +
, +] +`; + +exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ Manage workpad assets +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+

+ Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets. +

+
+
+
+
+
+
+
+
+ Asset thumbnail +
+
+
+
+

+ + airplane + +
+ + + ( + 1 + kb) + + +

+
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ Asset thumbnail +
+
+
+
+

+ + marker + +
+ + + ( + 1 + kb) + + +

+
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+ 0% space used +
+
+
+ +
+
+
+
, +
, +] +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx deleted file mode 100644 index 045a3b4f64a44..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx +++ /dev/null @@ -1,47 +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 { action } from '@storybook/addon-actions'; -import { storiesOf } from '@storybook/react'; -import React from 'react'; -import { AssetType } from '../../../../types'; -import { AssetManager } from '../asset_manager'; - -const AIRPLANE: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'airplane', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -const MARKER: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'marker', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -storiesOf('components/Assets/AssetManager', module) - .add('no assets', () => ( - // @ts-ignore @types/react has not been updated to support defaultProps yet. - - )) - .add('two assets', () => ( - - )); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx new file mode 100644 index 0000000000000..cb42823ccab7b --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.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 { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { AssetType } from '../../../../types'; +import { AssetManager } from '../asset_manager'; + +const AIRPLANE: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'airplane', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +const MARKER: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'marker', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +storiesOf('components/Assets/AssetManager', module) + .add('no assets', () => ( + + )) + .add('two assets', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx index 3785f81cc25b9..c27f0c002c3d1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx @@ -4,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore (elastic/eui#1557) EuiFilePicker is not exported yet - EuiFilePicker, - // @ts-ignore (elastic/eui#1557) EuiImage is not exported yet - EuiImage, -} from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { Fragment, PureComponent } from 'react'; @@ -22,7 +15,7 @@ import { AssetModal } from './asset_modal'; const { AssetManager: strings } = ComponentStrings; -interface Props { +export interface Props { /** A list of assets, if available */ assetValues: AssetType[]; /** Function to invoke when an asset is selected to be added as an element to the workpad */ @@ -33,13 +26,13 @@ interface Props { onAssetCopy: () => void; /** Function to invoke when an asset is added */ onAssetAdd: (asset: File) => void; + /** Function to invoke when an asset modal is closed */ + onClose: () => void; } interface State { /** The id of the asset to delete, if applicable. Is set if the viewer clicks the delete icon */ deleteId: string | null; - /** Determines if the modal is currently visible */ - isModalVisible: boolean; /** Indicates if the modal is currently loading */ isLoading: boolean; } @@ -51,6 +44,7 @@ export class AssetManager extends PureComponent { onAssetAdd: PropTypes.func.isRequired, onAssetCopy: PropTypes.func.isRequired, onAssetDelete: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, }; public static defaultProps = { @@ -60,12 +54,11 @@ export class AssetManager extends PureComponent { public state = { deleteId: null, isLoading: false, - isModalVisible: false, }; public render() { - const { isModalVisible, isLoading } = this.state; - const { assetValues, onAssetCopy, onAddImageElement } = this.props; + const { isLoading } = this.state; + const { assetValues, onAssetCopy, onAddImageElement, onClose } = this.props; const assetModal = ( { onAssetCopy={onAssetCopy} onAssetCreate={(createdAsset: AssetType) => { onAddImageElement(createdAsset.id); - this.setState({ isModalVisible: false }); + onClose(); }} onAssetDelete={(asset: AssetType) => this.setState({ deleteId: asset.id })} - onClose={() => this.setState({ isModalVisible: false })} + onClose={onClose} onFileUpload={this.handleFileUpload} /> ); @@ -95,14 +88,12 @@ export class AssetManager extends PureComponent { return ( - {strings.getButtonLabel()} - {isModalVisible ? assetModal : null} + {assetModal} {confirmModal} ); } - private showModal = () => this.setState({ isModalVisible: true }); private resetDelete = () => this.setState({ deleteId: null }); private doDelete = () => { diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx index 3dfbb1b1fde3c..8637db8e9f962 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx @@ -98,6 +98,7 @@ export const AssetModal: FunctionComponent = props => { ({ - assets: getAssets(state), - selectedPage: getSelectedPage(state), -}); - -const mapDispatchToProps = dispatch => ({ - onAddImageElement: pageId => assetId => { - const imageElement = elementsRegistry.get('image'); - const elementAST = fromExpression(imageElement.expression); - const selector = ['chain', '0', 'arguments', 'dataurl']; - const subExp = [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'asset', - arguments: { - _: [assetId], - }, - }, - ], - }, - ]; - const newAST = set(elementAST, selector, subExp); - imageElement.expression = toExpression(newAST); - dispatch(addElement(pageId, imageElement)); - }, - onAssetAdd: (type, content) => { - // make the ID here and pass it into the action - const assetId = getId('asset'); - dispatch(createAsset(type, content, assetId)); - - // then return the id, so the caller knows the id that will be created - return assetId; - }, - onAssetDelete: assetId => dispatch(removeAsset(assetId)), -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { assets, selectedPage } = stateProps; - const { onAssetAdd } = dispatchProps; - const assetValues = Object.values(assets); // pull values out of assets object - - return { - ...ownProps, - ...dispatchProps, - onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage), - selectedPage, - assetValues, - onAssetAdd: file => { - const [type, subtype] = get(file, 'type', '').split('/'); - if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { - return encode(file).then(dataurl => { - const type = 'dataurl'; - const existingId = findExistingAsset(type, dataurl, assetValues); - if (existingId) { - return existingId; - } - return onAssetAdd(type, dataurl); - }); - } - - return false; - }, - }; -}; - -export const AssetManager = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps({ onAssetCopy: asset => notify.success(`Copied '${asset.id}' to clipboard`) }) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.ts b/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.ts new file mode 100644 index 0000000000000..3fd34d6d2a9bb --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.ts @@ -0,0 +1,109 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { set, get } from 'lodash'; +import { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { getAssets } from '../../state/selectors/assets'; +// @ts-ignore Untyped local +import { removeAsset, createAsset } from '../../state/actions/assets'; +// @ts-ignore Untyped local +import { elementsRegistry } from '../../lib/elements_registry'; +// @ts-ignore Untyped local +import { addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { encode } from '../../../common/lib/dataurl'; +import { getId } from '../../lib/get_id'; +// @ts-ignore Untyped Local +import { findExistingAsset } from '../../lib/find_existing_asset'; +import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { WithKibanaProps } from '../../'; +import { AssetManager as Component, Props as AssetManagerProps } from './asset_manager'; + +import { State, ExpressionAstExpression, AssetType } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + assets: getAssets(state), + selectedPage: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: (action: any) => void) => ({ + onAddImageElement: (pageId: string) => (assetId: string) => { + const imageElement = elementsRegistry.get('image'); + const elementAST = fromExpression(imageElement.expression); + const selector = ['chain', '0', 'arguments', 'dataurl']; + const subExp: ExpressionAstExpression[] = [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'asset', + arguments: { + _: [assetId], + }, + }, + ], + }, + ]; + const newAST = set(elementAST, selector, subExp); + imageElement.expression = toExpression(newAST); + dispatch(addElement(pageId, imageElement)); + }, + onAssetAdd: (type: string, content: string) => { + // make the ID here and pass it into the action + const assetId = getId('asset'); + dispatch(createAsset(type, content, assetId)); + + // then return the id, so the caller knows the id that will be created + return assetId; + }, + onAssetDelete: (assetId: string) => dispatch(removeAsset(assetId)), +}); + +const mergeProps = ( + stateProps: ReturnType, + dispatchProps: ReturnType, + ownProps: AssetManagerProps +) => { + const { assets, selectedPage } = stateProps; + const { onAssetAdd } = dispatchProps; + const assetValues = Object.values(assets); // pull values out of assets object + + return { + ...ownProps, + ...dispatchProps, + onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage), + selectedPage, + assetValues, + onAssetAdd: (file: File) => { + const [type, subtype] = get(file, 'type', '').split('/'); + if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { + return encode(file).then(dataurl => { + const dataurlType = 'dataurl'; + const existingId = findExistingAsset(dataurlType, dataurl, assetValues); + if (existingId) { + return existingId; + } + return onAssetAdd(dataurlType, dataurl); + }); + } + + return false; + }, + }; +}; + +export const AssetManager = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withProps(({ kibana }: WithKibanaProps) => ({ + onAssetCopy: (asset: AssetType) => + kibana.services.canvas.notify.success(`Copied '${asset.id}' to clipboard`), + })) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot deleted file mode 100644 index 6572aa582f48c..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot +++ /dev/null @@ -1,797 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- sample description -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-
- -
-
- - - -
-

- Aenean eu justo auctor, placerat felis non, scelerisque dolor. -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-
- -
-
- - - -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid with controls and filter 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid with tags filter 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- A static image -

-
-
-
- - - - graphic - - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid with text filter 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- A scrollable grid for displaying data in a tabular format -

-
-
-
- - - - text - - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid without controls 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- A line chart with a filled body -

-
-
-
- - - - chart - - - -
-
-
-
-
-
- -
-
- - - -
-

- A static image -

-
-
-
- - - - graphic - - - -
-
-
-
-
-
- -
-
- - - -
-

- A scrollable grid for displaying data in a tabular format -

-
-
-
- - - - text - - - -
-
-
-
-
-`; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_grid.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_grid.stories.tsx deleted file mode 100644 index d12c00bc5dd2e..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_grid.stories.tsx +++ /dev/null @@ -1,58 +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 React from 'react'; -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import { ElementGrid } from '../element_grid'; -import { testElements, testCustomElements } from './fixtures/test_elements'; - -storiesOf('components/Elements/ElementGrid', module) - .addDecorator(story => ( -
- {story()} -
- )) - .add('without controls', () => ( - - )) - .add('with controls', () => ( - - )) - .add('with text filter', () => ( - - )) - .add('with tags filter', () => ( - - )) - .add('with controls and filter', () => ( - - )); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx b/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx deleted file mode 100644 index 852b987fcfd24..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx +++ /dev/null @@ -1,110 +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 React from 'react'; -import PropTypes from 'prop-types'; -import { map } from 'lodash'; -import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui'; -import { ElementControls } from './element_controls'; -import { CustomElement, ElementSpec } from '../../../types'; -import { ElementCard } from '../element_card'; - -export interface Props { - /** - * list of elements to generate cards for - */ - elements: Array; - /** - * text to filter out cards - */ - filterText: string; - /** - * tags to filter out cards - */ - filterTags: string[]; - /** - * indicate whether or not edit/delete controls should be displayed - */ - showControls: boolean; - /** - * handler invoked when clicking a card - */ - handleClick: (element: ElementSpec | CustomElement) => void; - /** - * click handler for the edit button - */ - onEdit?: (element: ElementSpec | CustomElement) => void; - /** - * click handler for the delete button - */ - onDelete?: (element: ElementSpec | CustomElement) => void; -} - -export const ElementGrid = ({ - elements, - filterText, - filterTags, - handleClick, - onEdit, - onDelete, - showControls, -}: Props) => { - filterText = filterText.toLowerCase(); - - return ( - - {map(elements, (element: ElementSpec | CustomElement, index) => { - const { name, displayName = '', help = '', image, tags = [] } = element; - const whenClicked = () => handleClick(element); - let textMatch = false; - let tagsMatch = false; - - if ( - !filterText.length || - name.toLowerCase().includes(filterText) || - displayName.toLowerCase().includes(filterText) || - help.toLowerCase().includes(filterText) - ) { - textMatch = true; - } - - if (!filterTags.length || filterTags.every(tag => tags.includes(tag))) { - tagsMatch = true; - } - - if (!textMatch || !tagsMatch) { - return null; - } - - return ( - - - {showControls && onEdit && onDelete && ( - onEdit(element)} onDelete={() => onDelete(element)} /> - )} - - ); - })} - - ); -}; - -ElementGrid.propTypes = { - elements: PropTypes.array.isRequired, - handleClick: PropTypes.func.isRequired, - showControls: PropTypes.bool, -}; - -ElementGrid.defaultProps = { - showControls: false, - filterTags: [], - filterText: '', -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js b/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js deleted file mode 100644 index dabf06a24aeb6..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js +++ /dev/null @@ -1,224 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiModalBody, - EuiTabbedContent, - EuiEmptyPrompt, - EuiSearchBar, - EuiSpacer, - EuiOverlayMask, -} from '@elastic/eui'; -import { map, sortBy } from 'lodash'; -import { ConfirmModal } from '../confirm_modal/confirm_modal'; -import { CustomElementModal } from '../custom_element_modal'; -import { getTagsFilter } from '../../lib/get_tags_filter'; -import { extractSearch } from '../../lib/extract_search'; -import { ComponentStrings } from '../../../i18n'; -import { ElementGrid } from './element_grid'; - -const { ElementTypes: strings } = ComponentStrings; - -const tagType = 'badge'; -export class ElementTypes extends Component { - static propTypes = { - addCustomElement: PropTypes.func.isRequired, - addElement: PropTypes.func.isRequired, - customElements: PropTypes.array.isRequired, - elements: PropTypes.object, - filterTags: PropTypes.arrayOf(PropTypes.string).isRequired, - findCustomElements: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - removeCustomElement: PropTypes.func.isRequired, - search: PropTypes.string, - setCustomElements: PropTypes.func.isRequired, - setSearch: PropTypes.func.isRequired, - setFilterTags: PropTypes.func.isRequired, - updateCustomElement: PropTypes.func.isRequired, - }; - - state = { - elementToDelete: null, - elementToEdit: null, - }; - - componentDidMount() { - // fetch custom elements - this.props.findCustomElements(); - } - - _showEditModal = elementToEdit => this.setState({ elementToEdit }); - - _hideEditModal = () => this.setState({ elementToEdit: null }); - - _handleEdit = async (name, description, image) => { - const { updateCustomElement } = this.props; - const { elementToEdit } = this.state; - await updateCustomElement(elementToEdit.id, name, description, image); - this._hideEditModal(); - }; - - _showDeleteModal = elementToDelete => this.setState({ elementToDelete }); - - _hideDeleteModal = () => this.setState({ elementToDelete: null }); - - _handleDelete = async () => { - const { removeCustomElement } = this.props; - const { elementToDelete } = this.state; - await removeCustomElement(elementToDelete.id); - this._hideDeleteModal(); - }; - - _renderEditModal = () => { - const { elementToEdit } = this.state; - - if (!elementToEdit) { - return null; - } - - return ( - - - - ); - }; - - _renderDeleteModal = () => { - const { elementToDelete } = this.state; - - if (!elementToDelete) { - return null; - } - - return ( - - ); - }; - - _sortElements = elements => - sortBy( - map(elements, (element, name) => ({ name, ...element })), - 'displayName' - ); - - render() { - const { - search, - setSearch, - addElement, - addCustomElement, - filterTags, - setFilterTags, - } = this.props; - let { elements, customElements } = this.props; - elements = this._sortElements(elements); - - let customElementContent = ( - {strings.getAddNewElementTitle()}} - body={

{strings.getAddNewElementDescription()}

} - titleSize="s" - /> - ); - - if (customElements.length) { - customElements = this._sortElements(customElements); - customElementContent = ( - - ); - } - - const filters = [getTagsFilter(tagType)]; - const onSearch = ({ queryText }) => { - const { searchTerm, filterTags } = extractSearch(queryText); - setSearch(searchTerm); - setFilterTags(filterTags); - }; - - const tabs = [ - { - id: 'elements', - name: strings.getElementsTitle(), - content: ( -
- - - - -
- ), - }, - { - id: 'customElements', - name: strings.getMyElementsTitle(), - content: ( - - - - - {customElementContent} - - ), - }, - ]; - - return ( - - - - - - {this._renderDeleteModal()} - {this._renderEditModal()} - - ); - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js deleted file mode 100644 index 8faaf278a07de..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js +++ /dev/null @@ -1,103 +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 PropTypes from 'prop-types'; -import { compose, withProps, withState } from 'recompose'; -import { connect } from 'react-redux'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { elementsRegistry } from '../../lib/elements_registry'; -import { notify } from '../../lib/notify'; -import { selectToplevelNodes } from '../../state/actions/transient'; -import { insertNodes, addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { ElementTypes as Component } from './element_types'; - -const customElementAdded = 'elements-custom-added'; - -const mapStateToProps = state => ({ pageId: getSelectedPage(state) }); - -const mapDispatchToProps = dispatch => ({ - selectToplevelNodes: nodes => - dispatch(selectToplevelNodes(nodes.filter(e => !e.position.parent).map(e => e.id))), - insertNodes: (selectedNodes, pageId) => dispatch(insertNodes(selectedNodes, pageId)), - addElement: (pageId, partialElement) => dispatch(addElement(pageId, partialElement)), -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { pageId, ...remainingStateProps } = stateProps; - const { addElement, insertNodes, selectToplevelNodes } = dispatchProps; - const { search, setCustomElements, onClose } = ownProps; - - return { - ...remainingStateProps, - ...ownProps, - // add built-in element to the page - addElement: element => { - addElement(pageId, element); - onClose(); - }, - // add custom element to the page - addCustomElement: customElement => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async text => { - try { - const { customElements } = await customElementService.find(text); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't find custom elements` }); - } - }, - // remove custom element - removeCustomElement: async id => { - try { - await customElementService.remove(id).then(); - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't delete custom elements` }); - } - }, - // update custom element - updateCustomElement: async (id, name, description, image) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't update custom elements` }); - } - }, - }; -}; - -export const ElementTypes = compose( - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - withState('filterTags', 'setFilterTags', []), - withProps(() => ({ elements: elementsRegistry.toJS() })), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); - -ElementTypes.propTypes = { - onClose: PropTypes.func, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/popover/index.ts b/x-pack/legacy/plugins/canvas/public/components/popover/index.ts index f560da14079b5..63626f08fa43b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/popover/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/popover/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Popover } from './popover'; +export { Popover, ClosePopoverFn } from './popover'; diff --git a/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx b/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx index 25b2e6587c869..9f3d86576e6a7 100644 --- a/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx @@ -24,6 +24,8 @@ interface Props { className?: string; } +export type ClosePopoverFn = () => void; + interface State { isPopoverOpen: boolean; } @@ -61,7 +63,7 @@ export class Popover extends Component { })); }; - closePopover = () => { + closePopover: ClosePopoverFn = () => { this.setState({ isPopoverOpen: false, }); diff --git a/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js b/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js index 68c3ba79dd488..cc234d2287c0c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js @@ -7,7 +7,7 @@ import { compose, withProps, withPropsOnChange } from 'recompose'; import PropTypes from 'prop-types'; import isEqual from 'react-fast-compare'; -import { notify } from '../../lib/notify'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { RenderWithFn as Component } from './render_with_fn'; import { ElementHandlers } from './lib/handlers'; @@ -19,9 +19,10 @@ export const RenderWithFn = compose( handlers: Object.assign(new ElementHandlers(), handlers), }) ), - withProps({ - onError: notify.error, - }) + withKibana, + withProps(props => ({ + onError: props.kibana.services.canvas.notify.error, + })) )(Component); RenderWithFn.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot similarity index 88% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot index 5ce6fe8c85589..6f12f68356467 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots components/Elements/ElementControls has two buttons 1`] = ` +exports[`Storyshots components/SavedElementsModal/ElementControls has two buttons 1`] = `
+
+
+
+
+ +
+
+ + + +
+

+ sample description +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Aenean eu justo auctor, placerat felis non, scelerisque dolor. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+`; + +exports[`Storyshots components/SavedElementsModal/ElementGrid with text filter 1`] = ` +
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot new file mode 100644 index 0000000000000..c73309ad8b14c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot @@ -0,0 +1,933 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ My elements +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ +

+ Add new elements +

+
+
+

+ Group and save workpad elements to create new elements +

+
+ +
+
+
+
+ +
+
+
+
, +
, +] +`; + +exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ My elements +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ +
+
+ + + +
+

+ sample description +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Aenean eu justo auctor, placerat felis non, scelerisque dolor. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
, +
, +] +`; + +exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ My elements +
+
+
+
+
+
+ +
+ + +
+ +
+
+
+
+
+
+
+
+ +
+
+ + + +
+

+ Aenean eu justo auctor, placerat felis non, scelerisque dolor. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
, +
, +] +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx similarity index 90% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx index 52736ac952e53..5210210ebaa74 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { ElementControls } from '../element_controls'; -storiesOf('components/Elements/ElementControls', module) +storiesOf('components/SavedElementsModal/ElementControls', module) .addDecorator(story => (
( +
+ {story()} +
+ )) + .add('default', () => ( + + )) + .add('with text filter', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx index eec7a86d52f25..d1ff565b4955a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx @@ -6,41 +6,6 @@ import { elasticLogo } from '../../../../lib/elastic_logo'; -export const testElements = [ - { - name: 'areaChart', - displayName: 'Area chart', - help: 'A line chart with a filled body', - tags: ['chart'], - image: elasticLogo, - expression: `filters - | demodata - | pointseries x="time" y="mean(price)" - | plot defaultStyle={seriesStyle lines=1 fill=1} - | render`, - }, - { - name: 'image', - displayName: 'Image', - help: 'A static image', - tags: ['graphic'], - image: elasticLogo, - expression: `image dataurl=null mode="contain" - | render`, - }, - { - name: 'table', - displayName: 'Data table', - tags: ['text'], - help: 'A scrollable grid for displaying data in a tabular format', - image: elasticLogo, - expression: `filters - | demodata - | table - | render`, - }, -]; - export const testCustomElements = [ { id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx new file mode 100644 index 0000000000000..4941d8cb2efa7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx @@ -0,0 +1,50 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { SavedElementsModal } from '../saved_elements_modal'; +import { testCustomElements } from './fixtures/test_elements'; +import { CustomElement } from '../../../../types'; + +storiesOf('components/SavedElementsModal', module) + .add('no custom elements', () => ( + + )) + .add('with custom elements', () => ( + + )) + .add('with text filter', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx similarity index 85% rename from x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx index a23274296f64f..998b15c15f487 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx @@ -30,7 +30,12 @@ export const ElementControls: FunctionComponent = ({ onDelete, onEdit }) > - + @@ -40,6 +45,7 @@ export const ElementControls: FunctionComponent = ({ onDelete, onEdit }) iconType="trash" aria-label={strings.getDeleteAriaLabel()} onClick={onDelete} + data-test-subj="canvasElementCard__deleteButton" /> diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx new file mode 100644 index 0000000000000..f86e2c0147035 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx @@ -0,0 +1,81 @@ +/* + * 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 PropTypes from 'prop-types'; +import { map } from 'lodash'; +import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui'; +import { ElementControls } from './element_controls'; +import { CustomElement } from '../../../types'; +import { ElementCard } from '../element_card'; + +export interface Props { + /** + * list of elements to generate cards for + */ + elements: CustomElement[]; + /** + * text to filter out cards + */ + filterText: string; + /** + * handler invoked when clicking a card + */ + onClick: (element: CustomElement) => void; + /** + * click handler for the edit button + */ + onEdit: (element: CustomElement) => void; + /** + * click handler for the delete button + */ + onDelete: (element: CustomElement) => void; +} + +export const ElementGrid = ({ elements, filterText, onClick, onEdit, onDelete }: Props) => { + filterText = filterText.toLowerCase(); + + return ( + + {map(elements, (element: CustomElement, index) => { + const { name, displayName = '', help = '', image } = element; + const whenClicked = () => onClick(element); + + if ( + filterText.length && + !name.toLowerCase().includes(filterText) && + !displayName.toLowerCase().includes(filterText) && + !help.toLowerCase().includes(filterText) + ) { + return null; + } + + return ( + + + onEdit(element)} onDelete={() => onDelete(element)} /> + + ); + })} + + ); +}; + +ElementGrid.propTypes = { + elements: PropTypes.array.isRequired, + filterText: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, +}; + +ElementGrid.defaultProps = { + filterText: '', +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts new file mode 100644 index 0000000000000..60d1d7462daa9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -0,0 +1,135 @@ +/* + * 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 { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { compose, withState } from 'recompose'; +import { camelCase } from 'lodash'; +// @ts-ignore Untyped local +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { WithKibanaProps } from '../../'; +// @ts-ignore Untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-ignore Untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { SavedElementsModal as Component, Props as ComponentProps } from './saved_elements_modal'; +import { State, PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +interface OwnProps { + onClose: () => void; +} + +interface OwnPropsWithState extends OwnProps { + customElements: CustomElement[]; + setCustomElements: (customElements: CustomElement[]) => void; + search: string; + setSearch: (search: string) => void; +} + +interface DispatchProps { + selectToplevelNodes: (nodes: PositionedElement[]) => void; + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; +} + +interface StateProps { + pageId: string; +} + +const mapStateToProps = (state: State): StateProps => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ), + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnPropsWithState & WithKibanaProps +): ComponentProps => { + const { pageId } = stateProps; + const { onClose, search, setCustomElements } = ownProps; + + const findCustomElements = async () => { + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + }; + + return { + ...ownProps, + // add custom element to the page + addCustomElement: (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }, + // custom element search + findCustomElements: async (text?: string) => { + try { + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't find custom elements`, + }); + } + }, + // remove custom element + removeCustomElement: async (id: string) => { + try { + await customElementService.remove(id); + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }, + // update custom element + updateCustomElement: async (id: string, name: string, description: string, image: string) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't update custom elements`, + }); + } + }, + }; +}; + +export const SavedElementsModal = compose( + withKibana, + withState('search', 'setSearch', ''), + withState('customElements', 'setCustomElements', []), + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx new file mode 100644 index 0000000000000..dba97a15fee5c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx @@ -0,0 +1,217 @@ +/* + * 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, { Fragment, ChangeEvent, FunctionComponent, useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiEmptyPrompt, + EuiFieldSearch, + EuiSpacer, + EuiOverlayMask, + EuiButton, +} from '@elastic/eui'; +import { map, sortBy } from 'lodash'; +import { ComponentStrings } from '../../../i18n'; +import { CustomElement } from '../../../types'; +import { ConfirmModal } from '../confirm_modal/confirm_modal'; +import { CustomElementModal } from '../custom_element_modal'; +import { ElementGrid } from './element_grid'; + +const { SavedElementsModal: strings } = ComponentStrings; + +export interface Props { + /** + * Adds the custom element to the workpad + */ + addCustomElement: (customElement: CustomElement) => void; + /** + * Queries ES for custom element saved objects + */ + findCustomElements: () => void; + /** + * Handler invoked when the modal closes + */ + onClose: () => void; + /** + * Deletes the custom element + */ + removeCustomElement: (id: string) => void; + /** + * Saved edits to the custom element + */ + updateCustomElement: (id: string, name: string, description: string, image: string) => void; + /** + * Array of custom elements to display + */ + customElements: CustomElement[]; + /** + * Text used to filter custom elements list + */ + search: string; + /** + * Setter for search text + */ + setSearch: (search: string) => void; +} + +export const SavedElementsModal: FunctionComponent = ({ + search, + setSearch, + customElements, + addCustomElement, + findCustomElements, + onClose, + removeCustomElement, + updateCustomElement, +}) => { + const [elementToDelete, setElementToDelete] = useState(null); + const [elementToEdit, setElementToEdit] = useState(null); + + useEffect(() => { + findCustomElements(); + }); + + const showEditModal = (element: CustomElement) => setElementToEdit(element); + const hideEditModal = () => setElementToEdit(null); + + const handleEdit = async (name: string, description: string, image: string) => { + if (elementToEdit) { + await updateCustomElement(elementToEdit.id, name, description, image); + } + hideEditModal(); + }; + + const showDeleteModal = (element: CustomElement) => setElementToDelete(element); + const hideDeleteModal = () => setElementToDelete(null); + + const handleDelete = async () => { + if (elementToDelete) { + await removeCustomElement(elementToDelete.id); + } + hideDeleteModal(); + }; + + const renderEditModal = () => { + if (!elementToEdit) { + return null; + } + + return ( + + + + ); + }; + + const renderDeleteModal = () => { + if (!elementToDelete) { + return null; + } + + return ( + + ); + }; + + const sortElements = (elements: CustomElement[]): CustomElement[] => + sortBy( + map(elements, (element, name) => ({ name, ...element })), + 'displayName' + ); + + const onSearch = (e: ChangeEvent) => setSearch(e.target.value); + + let customElementContent = ( + {strings.getAddNewElementTitle()}} + body={

{strings.getAddNewElementDescription()}

} + titleSize="s" + /> + ); + + if (customElements.length) { + customElementContent = ( + + ); + } + + return ( + + + + + + {strings.getModalTitle()} + + + + + + + {customElementContent} + + + + {strings.getSavedElementsModalCloseButtonLabel()} + + + + + + {renderDeleteModal()} + {renderEditModal()} + + ); +}; + +SavedElementsModal.propTypes = { + addCustomElement: PropTypes.func.isRequired, + findCustomElements: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + removeCustomElement: PropTypes.func.isRequired, + updateCustomElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot index d46a509251d35..ac25cbe0b6832 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot @@ -37,6 +37,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` + +
+
+ + + +
+
+ + + +
+
+ + + +
@@ -134,6 +235,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] +
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx new file mode 100644 index 0000000000000..9aca5ce33ba02 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx @@ -0,0 +1,150 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ElementSpec } from '../../../../../types'; +import { ElementMenu } from '../element_menu'; + +const testElements: { [key: string]: ElementSpec } = { + areaChart: { + name: 'areaChart', + displayName: 'Area chart', + help: 'A line chart with a filled body', + type: 'chart', + expression: `filters + | demodata + | pointseries x="time" y="mean(price)" + | plot defaultStyle={seriesStyle lines=1 fill=1} + | render`, + }, + debug: { + name: 'debug', + displayName: 'Debug data', + help: 'Just dumps the configuration of the element', + icon: 'bug', + expression: `demodata + | render as=debug`, + }, + dropdownFilter: { + name: 'dropdownFilter', + displayName: 'Dropdown select', + type: 'filter', + help: 'A dropdown from which you can select values for an "exactly" filter', + icon: 'filter', + height: 50, + expression: `demodata + | dropdownControl valueColumn=project filterColumn=project | render`, + filter: '', + }, + filterDebug: { + name: 'filterDebug', + displayName: 'Debug filter', + help: 'Shows the underlying global filters in a workpad', + icon: 'bug', + expression: `filters + | render as=debug`, + }, + image: { + name: 'image', + displayName: 'Image', + help: 'A static image', + type: 'image', + expression: `image dataurl=null mode="contain" + | render`, + }, + markdown: { + name: 'markdown', + displayName: 'Text', + type: 'text', + help: 'Add text using Markdown', + icon: 'visText', + expression: `filters +| demodata +| markdown "### Welcome to the Markdown element + +Good news! You're already connected to some demo data! + +The data table contains +**{{rows.length}} rows**, each containing +the following columns: +{{#each columns}} +**{{name}}** +{{/each}} + +You can use standard Markdown in here, but you can also access your piped-in data using Handlebars. If you want to know more, check out the [Handlebars documentation](https://handlebarsjs.com/guide/expressions.html). + +#### Enjoy!" | render`, + }, + progressGauge: { + name: 'progressGauge', + displayName: 'Gauge', + type: 'progress', + help: 'Displays progress as a portion of a gauge', + width: 200, + height: 200, + icon: 'visGoal', + expression: `filters + | demodata + | math "mean(percent_uptime)" + | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="Helvetica" color="#000000" align=center} + | render`, + }, + revealImage: { + name: 'revealImage', + displayName: 'Image reveal', + type: 'image', + help: 'Reveals a percentage of an image', + expression: `filters + | demodata + | math "mean(percent_uptime)" + | revealImage origin=bottom image=null + | render`, + }, + shape: { + name: 'shape', + displayName: 'Shape', + type: 'shape', + help: 'A customizable shape', + width: 200, + height: 200, + icon: 'node', + expression: + 'shape "square" fill="#4cbce4" border="rgba(255,255,255,0)" borderWidth=0 maintainAspect=false | render', + }, + table: { + name: 'table', + displayName: 'Data table', + type: 'chart', + help: 'A scrollable grid for displaying data in a tabular format', + expression: `filters + | demodata + | table + | render`, + }, + timeFilter: { + name: 'timeFilter', + displayName: 'Time filter', + type: 'filter', + help: 'Set a time window', + icon: 'calendar', + height: 50, + expression: `timefilterControl compact=true column=@timestamp + | render`, + filter: 'timefilter column=@timestamp from=now-24h to=now', + }, +}; + +const mockRenderEmbedPanel = () =>
; + +storiesOf('components/WorkpadHeader/ElementMenu', module).add('default', () => ( + +)); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss new file mode 100644 index 0000000000000..a946ee5519ce4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss @@ -0,0 +1,3 @@ +.canvasElementMenu__popoverButton { + margin-right: $euiSizeS; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx new file mode 100644 index 0000000000000..a416adfe77469 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -0,0 +1,216 @@ +/* + * 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 { sortBy } from 'lodash'; +import React, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ElementSpec } from '../../../../types'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { getId } from '../../../lib/get_id'; +import { Popover, ClosePopoverFn } from '../../popover'; +// @ts-ignore Untyped local +import { AssetManager } from '../../asset_manager'; +import { SavedElementsModal } from '../../saved_elements_modal'; + +interface CategorizedElementLists { + [key: string]: ElementSpec[]; +} + +interface ElementTypeMeta { + [key: string]: { name: string; icon: string }; +} + +export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; + +// label and icon for the context menu item for each element type +const elementTypeMeta: ElementTypeMeta = { + chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, + filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, + image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, + other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, + progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, + shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, + text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, +}; + +const getElementType = (element: ElementSpec): string => + element && element.type && Object.keys(elementTypeMeta).includes(element.type) + ? element.type + : 'other'; + +const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { + elements = sortBy(elements, 'displayName'); + + const categories: CategorizedElementLists = { other: [] }; + + elements.forEach((element: ElementSpec) => { + const type = getElementType(element); + + if (categories[type]) { + categories[type].push(element); + } else { + categories[type] = [element]; + } + }); + + return categories; +}; + +export interface Props { + /** + * Dictionary of elements from elements registry + */ + elements: { [key: string]: ElementSpec }; + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: ElementSpec) => void; + /** + * Renders embeddable flyout + */ + renderEmbedPanel: (onClose: () => void) => JSX.Element; +} + +export const ElementMenu: FunctionComponent = ({ + elements, + addElement, + renderEmbedPanel, +}) => { + const [isAssetModalVisible, setAssetModalVisible] = useState(false); + const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); + const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); + + const hideAssetModal = () => setAssetModalVisible(false); + const showAssetModal = () => setAssetModalVisible(true); + const showEmbedPanel = () => setEmbedPanelVisible(false); + const hideEmbedPanel = () => setEmbedPanelVisible(false); + const hideSavedElementsModal = () => setSavedElementsModalVisible(false); + const showSavedElementsModal = () => setSavedElementsModalVisible(true); + + const { + chart: chartElements, + filter: filterElements, + image: imageElements, + other: otherElements, + progress: progressElements, + shape: shapeElements, + text: textElements, + } = categorizeElementsByType(Object.values(elements)); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ + name: element.displayName || element.name, + icon: element.icon, + onClick: () => { + addElement(element); + closePopover(); + }, + }); + + const elementListToMenuItems = (elementList: ElementSpec[]) => { + const type = getElementType(elementList[0]); + const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; + + if (elementList.length > 1) { + return { + name, + icon: , + panel: { + id: getId('element-type'), + title: name, + items: elementList.map(elementToMenuItem), + }, + }; + } + + return elementToMenuItem(elementList[0]); + }; + + return { + id: 0, + title: strings.getElementMenuLabel(), + items: [ + elementListToMenuItems(textElements), + elementListToMenuItems(shapeElements), + elementListToMenuItems(chartElements), + elementListToMenuItems(imageElements), + elementListToMenuItems(filterElements), + elementListToMenuItems(progressElements), + elementListToMenuItems(otherElements), + { + name: strings.getMyElementsMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'saved-elements-menu-option', + icon: , + onClick: () => { + showSavedElementsModal(); + closePopover(); + }, + }, + { + name: strings.getAssetsMenuItemLabel(), + icon: , + onClick: () => { + showAssetModal(); + closePopover(); + }, + }, + { + name: strings.getEmbedObjectMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + onClick: () => { + showEmbedPanel(); + closePopover(); + }, + }, + ], + }; + }; + + const exportControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getElementMenuButtonLabel()} + + ); + + return ( + + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {isAssetModalVisible ? : null} + {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} + {isSavedElementsModalVisible ? : null} + + ); +}; + +ElementMenu.propTypes = { + elements: PropTypes.object, + addElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx new file mode 100644 index 0000000000000..40571a9341f69 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx @@ -0,0 +1,49 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { State, ElementSpec } from '../../../../types'; +// @ts-ignore Untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; +// @ts-ignore Untyped local +import { addElement } from '../../../state/actions/elements'; +import { getSelectedPage } from '../../../state/selectors/workpad'; +import { AddEmbeddablePanel } from '../../embeddable_flyout'; + +interface StateProps { + pageId: string; +} + +interface DispatchProps { + addElement: (pageId: string) => (partialElement: ElementSpec) => void; +} + +const mapStateToProps = (state: State) => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), +}); + +const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ + ...stateProps, + ...dispatchProps, + addElement: dispatchProps.addElement(stateProps.pageId), + // Moved this section out of the main component to enable stories + renderEmbedPanel: (onClose: () => void) => , +}); + +export const ElementMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withProps(() => ({ elements: elementsRegistry.toJS() })) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot index 43adcb37c5f4c..9ad2714a78ec9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots components/Export/PDFPanel default 1`] = ` +exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = `
+
+
+ +
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx similarity index 92% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx index 76a40f51148a7..eb99dbc494a32 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { PDFPanel } from '../pdf_panel'; -storiesOf('components/Export/PDFPanel', module) +storiesOf('components/WorkpadHeader/ShareMenu/PDFPanel', module) .addParameters({ info: { inline: true, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx new file mode 100644 index 0000000000000..ab9137b1676c9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx @@ -0,0 +1,20 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ShareMenu } from '../share_menu'; + +storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( + { + action(`getExportUrl('${type}')`); + return type; + }} + /> +)); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx index af30d8d4fc20b..886ddcfd763e1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { ShareWebsiteFlyout } from '../share_website_flyout'; -storiesOf('components/Export/ShareWebsiteFlyout', module) +storiesOf('components/WorkpadHeader/ShareMenu/ShareWebsiteFlyout', module) .addParameters({ info: { inline: true, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts new file mode 100644 index 0000000000000..4377635acac88 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -0,0 +1,99 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { + getWorkpad, + getRenderedWorkpad, + getRenderedWorkpadExpressions, +} from '../../../../state/selectors/workpad'; +// @ts-ignore Untyped local +import { + downloadRenderedWorkpad, + downloadRuntime, + downloadZippedRuntime, +} from '../../../../lib/download_workpad'; +import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; +import { State, CanvasWorkpad } from '../../../../../types'; +import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; +import { arrayBufferFetch } from '../../../../../common/lib/fetch'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; +import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; + +import { ComponentStrings } from '../../../../../i18n/components'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public/'; +import { OnCloseFn } from '../share_menu'; +import { WithKibanaProps } from '../../../../index'; +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +const getUnsupportedRenderers = (state: State) => { + const renderers: string[] = []; + const expressions = getRenderedWorkpadExpressions(state); + expressions.forEach(expression => { + if (!renderFunctionNames.includes(expression)) { + renderers.push(expression); + } + }); + + return renderers; +}; + +const mapStateToProps = (state: State) => ({ + renderedWorkpad: getRenderedWorkpad(state), + unsupportedRenderers: getUnsupportedRenderers(state), + workpad: getWorkpad(state), +}); + +interface Props { + onClose: OnCloseFn; + renderedWorkpad: CanvasRenderedWorkpad; + unsupportedRenderers: string[]; + workpad: CanvasWorkpad; +} + +export const ShareWebsiteFlyout = compose>( + connect(mapStateToProps), + withKibana, + withProps( + ({ + unsupportedRenderers, + renderedWorkpad, + onClose, + workpad, + kibana, + }: Props & WithKibanaProps): ComponentProps => ({ + unsupportedRenderers, + onClose, + onCopy: () => { + kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); + }, + onDownload: type => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(kibana.services.http.basePath.get()); + return; + case 'shareZip': + const basePath = kibana.services.http.basePath.get(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then(blob => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getShareableZipErrorTitle(workpad.name), + }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx similarity index 98% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx index 8dcbb18ffed86..5fd381baa73f5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ComponentStrings } from '../../../../../i18n/components'; import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants'; -import { OnCloseFn } from '../workpad_export'; +import { OnCloseFn } from '../share_menu'; import { WorkpadStep } from './workpad_step'; import { RuntimeStep } from './runtime_step'; import { SnippetsStep } from './snippets_step'; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx similarity index 98% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index c19ad6d77b131..81f559651eb25 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -42,7 +42,7 @@ export const SnippetsStep: FC<{ onCopy: OnCopyFn }> = ({ onCopy }) => ( ({ + workpad: getWorkpad(state), + pageCount: getPages(state).length, +}); + +const getAbsoluteUrl = (path: string) => { + const { location } = getWindow(); + + if (!location) { + return path; + } // fallback for mocked window object + + const { protocol, hostname, port } = location; + return `${protocol}//${hostname}:${port}${path}`; +}; + +interface Props { + workpad: CanvasWorkpad; + pageCount: number; +} + +export const ShareMenu = compose( + connect(mapStateToProps), + withKibana, + withProps( + ({ workpad, pageCount, kibana }: Props & WithKibanaProps): ComponentProps => ({ + getExportUrl: type => { + if (type === 'pdf') { + const pdfUrl = getPdfUrl(workpad, { pageCount }, kibana.services.http.basePath); + return getAbsoluteUrl(pdfUrl); + } + + throw new Error(strings.getUnknownExportErrorMessage(type)); + }, + onCopy: type => { + switch (type) { + case 'pdf': + kibana.services.canvas.notify.info(strings.getCopyPDFMessage()); + break; + case 'reportingConfig': + kibana.services.canvas.notify.info(strings.getCopyReportingConfigMessage()); + break; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + onExport: type => { + switch (type) { + case 'pdf': + return createPdf(workpad, { pageCount }, kibana.services.http.basePath) + .then(({ data }: { data: { job: { id: string } } }) => { + kibana.services.canvas.notify.info(strings.getExportPDFMessage(), { + title: strings.getExportPDFTitle(workpad.name), + }); + + // register the job so a completion notification shows up when it's ready + jobCompletionNotifications.add(data.job.id); + }) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getExportPDFErrorTitle(workpad.name), + }); + }); + case 'json': + downloadWorkpad(workpad.id); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx similarity index 92% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx index ef70079cf697b..a178964e0b566 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { Clipboard } from '../../clipboard'; import { ComponentStrings } from '../../../../i18n/components'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; interface Props { /** The URL that will invoke PDF Report generation. */ @@ -24,7 +24,7 @@ interface Props { * A panel displayed in the Export Menu with options in which to generate PDF Reports. */ export const PDFPanel = ({ pdfURL, onExport, onCopy }: Props) => ( -
+

{strings.getPDFPanelGenerateDescription()}

diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss new file mode 100644 index 0000000000000..03227f77e0de5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss @@ -0,0 +1,11 @@ +.canvasShareMenu__panelContent { + padding: $euiSize; +} + +.canvasShareMenu__reportingConfig { + .euiCodeBlock__pre { + @include euiScrollBar; + overflow-x: auto; + white-space: pre; + } +} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx new file mode 100644 index 0000000000000..621077c29c368 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -0,0 +1,122 @@ +/* + * 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, useState } from 'react'; +import PropTypes from 'prop-types'; +import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; +import { ComponentStrings } from '../../../../i18n/components'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { PDFPanel } from './pdf_panel'; +import { ShareWebsiteFlyout } from './flyout'; + +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +type CopyTypes = 'pdf' | 'reportingConfig'; +type ExportTypes = 'pdf' | 'json'; +type ExportUrlTypes = 'pdf'; +type CloseTypes = 'share'; + +export type OnCopyFn = (type: CopyTypes) => void; +export type OnExportFn = (type: ExportTypes) => void; +export type OnCloseFn = (type: CloseTypes) => void; +export type GetExportUrlFn = (type: ExportUrlTypes) => string; + +export interface Props { + /** Handler to invoke when an export URL is copied to the clipboard. */ + onCopy: OnCopyFn; + /** Handler to invoke when an end product is exported. */ + onExport: OnExportFn; + /** Handler to retrive an export URL based on the type of export requested. */ + getExportUrl: GetExportUrlFn; +} + +/** + * The Menu for Exporting a Workpad from Canvas. + */ +export const ShareMenu: FunctionComponent = ({ onCopy, onExport, getExportUrl }) => { + const [showFlyout, setShowFlyout] = useState(false); + + const onClose = () => { + setShowFlyout(false); + }; + + const getPDFPanel = (closePopover: ClosePopoverFn) => { + return ( + { + onExport('pdf'); + closePopover(); + }} + onCopy={() => { + onCopy('pdf'); + closePopover(); + }} + /> + ); + }; + + const getPanelTree = (closePopover: ClosePopoverFn) => ({ + id: 0, + title: strings.getShareWorkpadMessage(), + items: [ + { + name: strings.getShareDownloadJSONTitle(), + icon: , + onClick: () => { + onExport('json'); + closePopover(); + }, + }, + { + name: strings.getShareDownloadPDFTitle(), + icon: 'document', + panel: { + id: 1, + title: strings.getShareDownloadPDFTitle(), + content: getPDFPanel(closePopover), + }, + }, + { + name: strings.getShareWebsiteTitle(), + icon: , + onClick: () => { + setShowFlyout(true); + closePopover(); + }, + }, + ], + }); + + const shareControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getShareMenuButtonLabel()} + + ); + + const flyout = showFlyout ? : null; + + return ( +
+ + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {flyout} +
+ ); +}; + +ShareMenu.propTypes = { + onCopy: PropTypes.func.isRequired, + onExport: PropTypes.func.isRequired, + getExportUrl: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.ts diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot new file mode 100644 index 0000000000000..e1ecee0e152be --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/ViewMenu edit mode 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots components/WorkpadHeader/ViewMenu read only mode 1`] = ` +
+
+ +
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx new file mode 100644 index 0000000000000..60837ac1218e6 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx @@ -0,0 +1,39 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ViewMenu } from '../view_menu'; + +storiesOf('components/WorkpadHeader/ViewMenu', module) + .add('edit mode', () => ( + + )) + .add('read only mode', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts new file mode 100644 index 0000000000000..eee613183639c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -0,0 +1,83 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { Dispatch } from 'redux'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; +import { State, CanvasWorkpadBoundingBox } from '../../../../types'; +// @ts-ignore Untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +// @ts-ignore Untyped local +import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +// @ts-ignore Untyped local +import { setWriteable } from '../../../state/actions/workpad'; +import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; +import { + getWorkpadBoundingBox, + getWorkpadWidth, + getWorkpadHeight, + isWriteable, +} from '../../../state/selectors/workpad'; +import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; +import { getFitZoomScale } from './lib/get_fit_zoom_scale'; + +interface StateProps { + zoomScale: number; + boundingBox: CanvasWorkpadBoundingBox; + workpadWidth: number; + workpadHeight: number; + isWriteable: boolean; +} + +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; + setZoomScale: (scale: number) => void; + setFullscreen: (showFullscreen: boolean) => void; +} + +const mapStateToProps = (state: State) => ({ + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), + setFullscreen: (value: boolean) => { + dispatch(setFullscreen(value)); + if (value) { + dispatch(selectToplevelNodes([])); + } + }, + doRefresh: () => dispatch(fetchAllRenderables()), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + return { + ...remainingStateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + enterFullscreen: () => dispatchProps.setFullscreen(true), + fitToWindow: () => getFitZoomScale(boundingBox, workpadWidth, workpadHeight), + }; +}; + +export const ViewMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withHandlers(zoomHandlerCreators) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts new file mode 100644 index 0000000000000..783d6340c33c4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts @@ -0,0 +1,39 @@ +/* + * 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 { + CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR, + WORKPAD_CANVAS_BUFFER, +} from '../../../../../common/lib'; +import { CanvasWorkpadBoundingBox } from '../../../../../types'; + +export const getFitZoomScale = ( + boundingBox: CanvasWorkpadBoundingBox, + workpadWidth: number, + workpadHeight: number +) => { + const canvasLayoutContent = document.querySelector( + `#${CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}` + ) as HTMLElement; + const layoutWidth = canvasLayoutContent.clientWidth; + const layoutHeight = canvasLayoutContent.clientHeight; + const offsetLeft = boundingBox.left; + const offsetTop = boundingBox.top; + const offsetRight = boundingBox.right - workpadWidth; + const offsetBottom = boundingBox.bottom - workpadHeight; + const boundingWidth = + workpadWidth + + Math.max(Math.abs(offsetLeft), Math.abs(offsetRight)) * 2 + + WORKPAD_CANVAS_BUFFER; + const boundingHeight = + workpadHeight + + Math.max(Math.abs(offsetTop), Math.abs(offsetBottom)) * 2 + + WORKPAD_CANVAS_BUFFER; + const xScale = layoutWidth / boundingWidth; + const yScale = layoutHeight / boundingHeight; + + return Math.min(xScale, yScale); +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx new file mode 100644 index 0000000000000..d1e08c5809579 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx @@ -0,0 +1,172 @@ +/* + * 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'; +import PropTypes from 'prop-types'; +import { + EuiButtonEmpty, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../../../common/lib/constants'; +import { ComponentStrings } from '../../../../i18n/components'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; + +const { WorkpadHeaderViewMenu: strings } = ComponentStrings; + +const QUICK_ZOOM_LEVELS = [0.5, 1, 2]; + +export interface Props { + /** + * Is the workpad edittable? + */ + isWriteable: boolean; + /** + * current workpad zoom level + */ + zoomScale: number; + /** + * zooms to fit entire workpad into view + */ + fitToWindow: () => void; + /** + * handler to set the workpad zoom level to a specific value + */ + setZoomScale: (scale: number) => void; + /** + * handler to increase the workpad zoom level + */ + zoomIn: () => void; + /** + * handler to decrease workpad zoom level + */ + zoomOut: () => void; + /** + * reset zoom to 100% + */ + resetZoom: () => void; + /** + * toggle edit/read only mode + */ + toggleWriteable: () => void; + /** + * enter fullscreen mode + */ + enterFullscreen: () => void; + /** + * triggers a refresh of the workpad + */ + doRefresh: () => void; +} + +export const ViewMenu: FunctionComponent = ({ + doRefresh, + enterFullscreen, + fitToWindow, + isWriteable, + resetZoom, + setZoomScale, + toggleWriteable, + zoomIn, + zoomOut, + zoomScale, +}) => { + const viewControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getViewMenuButtonLabel()} + + ); + + const getScaleMenuItems = (): EuiContextMenuPanelItemDescriptor[] => + QUICK_ZOOM_LEVELS.map((scale: number) => ({ + name: strings.getZoomPercentage(scale), + icon: 'empty', + onClick: () => setZoomScale(scale), + })); + + const getZoomMenuItems = (): EuiContextMenuPanelItemDescriptor[] => [ + { + name: strings.getZoomFitToWindowText(), + icon: 'empty', + onClick: fitToWindow, + disabled: zoomScale === MAX_ZOOM_LEVEL, + }, + ...getScaleMenuItems(), + { + name: strings.getZoomInText(), + icon: 'magnifyWithPlus', + onClick: zoomIn, + disabled: zoomScale === MAX_ZOOM_LEVEL, + className: 'canvasContextMenu--topBorder', + }, + { + name: strings.getZoomOutText(), + icon: 'magnifyWithMinus', + onClick: zoomOut, + disabled: zoomScale <= MIN_ZOOM_LEVEL, + }, + { + name: strings.getZoomResetText(), + icon: 'empty', + onClick: resetZoom, + disabled: zoomScale >= MAX_ZOOM_LEVEL, + className: 'canvasContextMenu--topBorder', + }, + ]; + + const getPanelTree = (closePopover: ClosePopoverFn) => ({ + id: 0, + title: strings.getViewMenuLabel(), + items: [ + { + name: strings.getFullscreenMenuItemLabel(), + icon: , + onClick: () => { + enterFullscreen(); + closePopover(); + }, + }, + { + name: isWriteable ? strings.getHideEditModeLabel() : strings.getShowEditModeLabel(), + icon: , + onClick: () => { + toggleWriteable(); + closePopover(); + }, + }, + { + name: strings.getRefreshMenuItemLabel(), + icon: 'refresh', + onClick: () => { + doRefresh(); + }, + }, + { + name: strings.getZoomMenuItemLabel(), + icon: 'magnifyWithPlus', + panel: { + id: 1, + title: strings.getZoomMenuItemLabel(), + items: getZoomMenuItems(), + }, + }, + ], + }); + + return ( + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + ); +}; + +ViewMenu.propTypes = { + isWriteable: PropTypes.bool.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot deleted file mode 100644 index ef96320e7bc65..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/Export/WorkpadExport enabled 1`] = ` -
-
-
- - - -
-
-
-`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx deleted file mode 100644 index 92e7cca40ee3a..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx +++ /dev/null @@ -1,20 +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 { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import { WorkpadExport } from '../workpad_export'; - -storiesOf('components/Export/WorkpadExport', module).add('enabled', () => ( - { - action(`getExportUrl('${type}')`); - return type; - }} - /> -)); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts deleted file mode 100644 index 2bf3e1f0ef1f4..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts +++ /dev/null @@ -1,102 +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 { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -// @ts-ignore Untyped local -import { - getWorkpad, - getRenderedWorkpad, - getRenderedWorkpadExpressions, -} from '../../../../state/selectors/workpad'; -// @ts-ignore Untyped local -import { notify } from '../../../../lib/notify'; -// @ts-ignore Untyped local -import { - downloadRenderedWorkpad, - downloadRuntime, - downloadZippedRuntime, - // @ts-ignore Untyped local -} from '../../../../lib/download_workpad'; -import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; -import { State, CanvasWorkpad } from '../../../../../types'; -import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; -// @ts-ignore Untyped local. -import { fetch, arrayBufferFetch } from '../../../../../common/lib/fetch'; -import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; -import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; - -import { ComponentStrings } from '../../../../../i18n/components'; -import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public/'; -import { OnCloseFn } from '../workpad_export'; -import { WithKibanaProps } from '../../../../index'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; - -const getUnsupportedRenderers = (state: State) => { - const renderers: string[] = []; - const expressions = getRenderedWorkpadExpressions(state); - expressions.forEach(expression => { - if (!renderFunctionNames.includes(expression)) { - renderers.push(expression); - } - }); - - return renderers; -}; - -const mapStateToProps = (state: State) => ({ - renderedWorkpad: getRenderedWorkpad(state), - unsupportedRenderers: getUnsupportedRenderers(state), - workpad: getWorkpad(state), -}); - -interface Props { - onClose: OnCloseFn; - renderedWorkpad: CanvasRenderedWorkpad; - unsupportedRenderers: string[]; - workpad: CanvasWorkpad; -} - -export const ShareWebsiteFlyout = compose>( - connect(mapStateToProps), - withKibana, - withProps( - ({ - unsupportedRenderers, - renderedWorkpad, - onClose, - workpad, - kibana, - }: Props & WithKibanaProps): ComponentProps => ({ - unsupportedRenderers, - onClose, - onCopy: () => { - notify.info(strings.getCopyShareConfigMessage()); - }, - onDownload: type => { - switch (type) { - case 'share': - downloadRenderedWorkpad(renderedWorkpad); - return; - case 'shareRuntime': - downloadRuntime(kibana.services.http.basePath.get()); - return; - case 'shareZip': - const basePath = kibana.services.http.basePath.get(); - arrayBufferFetch - .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) - .then(blob => downloadZippedRuntime(blob.data)) - .catch((err: Error) => { - notify.error(err, { title: strings.getShareableZipErrorTitle(workpad.name) }); - }); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts deleted file mode 100644 index b0083eb4f87e2..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ /dev/null @@ -1,102 +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 { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; -// @ts-ignore Untyped local -import { getWorkpad, getPages } from '../../../state/selectors/workpad'; -// @ts-ignore Untyped local -import { notify } from '../../../lib/notify'; -import { getWindow } from '../../../lib/get_window'; -// @ts-ignore Untyped local -import { - downloadWorkpad, - // @ts-ignore Untyped local -} from '../../../lib/download_workpad'; -import { WorkpadExport as Component, Props as ComponentProps } from './workpad_export'; -import { getPdfUrl, createPdf } from './utils'; -import { State, CanvasWorkpad } from '../../../../types'; -// @ts-ignore Untyped local. -import { fetch, arrayBufferFetch } from '../../../../common/lib/fetch'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; -import { WithKibanaProps } from '../../../index'; - -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; - -const mapStateToProps = (state: State) => ({ - workpad: getWorkpad(state), - pageCount: getPages(state).length, -}); - -const getAbsoluteUrl = (path: string) => { - const { location } = getWindow(); - - if (!location) { - return path; - } // fallback for mocked window object - - const { protocol, hostname, port } = location; - return `${protocol}//${hostname}:${port}${path}`; -}; - -interface Props { - workpad: CanvasWorkpad; - pageCount: number; -} - -export const WorkpadExport = compose( - connect(mapStateToProps), - withKibana, - withProps( - ({ workpad, pageCount, kibana }: Props & WithKibanaProps): ComponentProps => ({ - getExportUrl: type => { - if (type === 'pdf') { - const pdfUrl = getPdfUrl(workpad, { pageCount }, kibana.services.http.basePath); - return getAbsoluteUrl(pdfUrl); - } - - throw new Error(strings.getUnknownExportErrorMessage(type)); - }, - onCopy: type => { - switch (type) { - case 'pdf': - notify.info(strings.getCopyPDFMessage()); - break; - case 'reportingConfig': - notify.info(strings.getCopyReportingConfigMessage()); - break; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - onExport: type => { - switch (type) { - case 'pdf': - return createPdf(workpad, { pageCount }, kibana.services.http.basePath) - .then(({ data }: { data: { job: { id: string } } }) => { - notify.info(strings.getExportPDFMessage(), { - title: strings.getExportPDFTitle(workpad.name), - }); - - // register the job so a completion notification shows up when it's ready - jobCompletionNotifications.add(data.job.id); - }) - .catch((err: Error) => { - notify.error(err, { title: strings.getExportPDFErrorTitle(workpad.name) }); - }); - case 'json': - downloadWorkpad(workpad.id); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss deleted file mode 100644 index 44209aaa72d63..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss +++ /dev/null @@ -1,11 +0,0 @@ -.canvasWorkpadExport__panelContent { - padding: $euiSize; -} - -.canvasWorkpadExport__reportingConfig { - .euiCodeBlock__pre { - @include euiScrollBar; - overflow-x: auto; - white-space: pre; - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx deleted file mode 100644 index 522be043ec457..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx +++ /dev/null @@ -1,149 +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 React, { FunctionComponent, useState } from 'react'; -import PropTypes from 'prop-types'; -import { EuiButtonIcon, EuiContextMenu, EuiIcon } from '@elastic/eui'; -// @ts-ignore Untyped local -import { Popover } from '../../popover'; -import { PDFPanel } from './pdf_panel'; -import { ShareWebsiteFlyout } from './flyout'; - -import { ComponentStrings } from '../../../../i18n/components'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; - -type ClosePopoverFn = () => void; - -type CopyTypes = 'pdf' | 'reportingConfig'; -type ExportTypes = 'pdf' | 'json'; -type ExportUrlTypes = 'pdf'; -type CloseTypes = 'share'; - -export type OnCopyFn = (type: CopyTypes) => void; -export type OnExportFn = (type: ExportTypes) => void; -export type OnCloseFn = (type: CloseTypes) => void; -export type GetExportUrlFn = (type: ExportUrlTypes) => string; - -export interface Props { - /** Handler to invoke when an export URL is copied to the clipboard. */ - onCopy: OnCopyFn; - /** Handler to invoke when an end product is exported. */ - onExport: OnExportFn; - /** Handler to retrive an export URL based on the type of export requested. */ - getExportUrl: GetExportUrlFn; -} - -/** - * The Menu for Exporting a Workpad from Canvas. - */ -export const WorkpadExport: FunctionComponent = ({ onCopy, onExport, getExportUrl }) => { - const [showFlyout, setShowFlyout] = useState(false); - - const onClose = () => { - setShowFlyout(false); - }; - - // TODO: Fix all of this magic from EUI; this code is boilerplate from - // EUI examples and isn't easily typed. - const flattenPanelTree = (tree: any, array: any[] = []) => { - array.push(tree); - - if (tree.items) { - tree.items.forEach((item: any) => { - const { panel } = item; - if (panel) { - flattenPanelTree(panel, array); - item.panel = panel.id; - } - }); - } - - return array; - }; - - const getPDFPanel = (closePopover: ClosePopoverFn) => { - return ( - { - onExport('pdf'); - closePopover(); - }} - onCopy={() => { - onCopy('pdf'); - closePopover(); - }} - /> - ); - }; - - const getPanelTree = (closePopover: ClosePopoverFn) => ({ - id: 0, - title: strings.getShareWorkpadMessage(), - items: [ - { - name: strings.getShareDownloadJSONTitle(), - icon: , - onClick: () => { - onExport('json'); - closePopover(); - }, - }, - { - name: strings.getShareDownloadPDFTitle(), - icon: 'document', - panel: { - id: 1, - title: strings.getShareDownloadPDFTitle(), - content: getPDFPanel(closePopover), - }, - }, - { - name: strings.getShareWebsiteTitle(), - icon: , - onClick: () => { - setShowFlyout(true); - closePopover(); - }, - }, - ], - }); - - const exportControl = (togglePopover: React.MouseEventHandler) => ( - - ); - - const flyout = showFlyout ? : null; - - return ( -
- - {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - - )} - - {flyout} -
- ); -}; - -WorkpadExport.propTypes = { - onCopy: PropTypes.func.isRequired, - onExport: PropTypes.func.isRequired, - getExportUrl: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx index 31ad0593f58bb..253e6c68cfc9e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -4,38 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; // @ts-ignore no @types definition import { Shortcuts } from 'react-shortcuts'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiButtonIcon, - EuiButton, - EuiButtonEmpty, - EuiOverlayMask, - EuiModal, - EuiModalFooter, - EuiToolTip, -} from '@elastic/eui'; - +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; - -// @ts-ignore untyped local -import { AssetManager } from '../asset_manager'; -// @ts-ignore untyped local -import { ElementTypes } from '../element_types'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { AddEmbeddablePanel } from '../embeddable_flyout'; -// @ts-ignore untyped local import { ControlSettings } from './control_settings'; // @ts-ignore untyped local import { RefreshControl } from './refresh_control'; // @ts-ignore untyped local import { FullscreenControl } from './fullscreen_control'; -import { WorkpadExport } from './workpad_export'; -import { WorkpadZoom } from './workpad_zoom'; +import { ElementMenu } from './element_menu'; +import { ShareMenu } from './share_menu'; +import { ViewMenu } from './view_menu'; const { WorkpadHeader: strings } = ComponentStrings; @@ -43,23 +26,20 @@ export interface Props { isWriteable: boolean; toggleWriteable: () => void; canUserWrite: boolean; - selectedPage: string; } -interface State { - isModalVisible: boolean; - isPanelVisible: boolean; -} - -export class WorkpadHeader extends React.PureComponent { - static propTypes = { - isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, +export const WorkpadHeader: FunctionComponent = ({ + isWriteable, + canUserWrite, + toggleWriteable, +}) => { + const keyHandler = (action: string) => { + if (action === 'EDITING') { + toggleWriteable(); + } }; - state = { isModalVisible: false, isPanelVisible: false }; - - _fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( + const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( { ); - _keyHandler = (action: string) => { - if (action === 'EDITING') { - this.props.toggleWriteable(); - } - }; - - _hideElementModal = () => this.setState({ isModalVisible: false }); - _showElementModal = () => this.setState({ isModalVisible: true }); - - _hideEmbeddablePanel = () => this.setState({ isPanelVisible: false }); - _showEmbeddablePanel = () => this.setState({ isPanelVisible: true }); - - _elementAdd = () => ( - - - - - - {strings.getAddElementModalCloseButtonLabel()} - - - - - ); - - _embeddableAdd = () => ; - - _getEditToggleToolTipText = () => { - if (!this.props.canUserWrite) { + const getEditToggleToolTipText = () => { + if (!canUserWrite) { return strings.getNoWritePermissionTooltipText(); } - const content = this.props.isWriteable + const content = isWriteable ? strings.getHideEditControlTooltip() : strings.getShowEditControlTooltip(); return content; }; - _getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { - const content = this._getEditToggleToolTipText(); + const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { + const content = getEditToggleToolTipText(); if (textOnly) { return content; @@ -135,86 +83,67 @@ export class WorkpadHeader extends React.PureComponent { ); }; - render() { - const { isWriteable, canUserWrite, toggleWriteable } = this.props; - const { isModalVisible, isPanelVisible } = this.state; - - return ( -
- {isModalVisible ? this._elementAdd() : null} - {isPanelVisible ? this._embeddableAdd() : null} - - - - - - - - - - - {this._fullscreenButton} - - - - - - - - - {canUserWrite && ( - - )} - - - - - - - {isWriteable ? ( + return ( + + + + {isWriteable && ( - - - - - - - {strings.getEmbedObjectButtonLabel()} - - - - - {strings.getAddElementButtonLabel()} - - - + - ) : null} + )} + + + + + + + + + -
- ); - } -} + + + + + {canUserWrite && ( + + )} + + + + + + + + + {fullscreenButton} + + + + + ); +}; + +WorkpadHeader.propTypes = { + isWriteable: PropTypes.bool, + toggleWriteable: PropTypes.func, + canUserWrite: PropTypes.bool, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx deleted file mode 100644 index b22a9d35aa793..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx +++ /dev/null @@ -1,38 +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 { compose, withHandlers } from 'recompose'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { getZoomScale } from '../../../state/selectors/app'; -import { - getWorkpadBoundingBox, - getWorkpadWidth, - getWorkpadHeight, -} from '../../../state/selectors/workpad'; -// @ts-ignore unconverted local file -import { setZoomScale } from '../../../state/actions/transient'; -import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; -import { WorkpadZoom as Component, Props as ComponentProps } from './workpad_zoom'; -import { State } from '../../../../types'; - -const mapStateToProps = (state: State) => { - return { - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), -}); - -export const WorkpadZoom = compose( - connect(mapStateToProps, mapDispatchToProps), - withHandlers(zoomHandlerCreators) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss deleted file mode 100644 index 44209aaa72d63..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss +++ /dev/null @@ -1,11 +0,0 @@ -.canvasWorkpadExport__panelContent { - padding: $euiSize; -} - -.canvasWorkpadExport__reportingConfig { - .euiCodeBlock__pre { - @include euiScrollBar; - overflow-x: auto; - white-space: pre; - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx deleted file mode 100644 index 4e37a525761cd..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx +++ /dev/null @@ -1,176 +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 React, { MouseEventHandler, PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiButtonIcon, - EuiContextMenu, - EuiContextMenuPanelDescriptor, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui'; -// @ts-ignore untyped local -import { Popover } from '../../popover'; -import { - MAX_ZOOM_LEVEL, - MIN_ZOOM_LEVEL, - WORKPAD_CANVAS_BUFFER, - CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR, -} from '../../../../common/lib/constants'; - -import { ComponentStrings } from '../../../../i18n'; -const { WorkpadHeaderWorkpadZoom: strings } = ComponentStrings; - -export interface Props { - /** - * current workpad zoom level - */ - zoomScale: number; - /** - * minimum bounding box for the workpad - */ - boundingBox: { left: number; right: number; top: number; bottom: number }; - /** - * width of the workpad page - */ - workpadWidth: number; - /** - * height of the workpad page - */ - workpadHeight: number; - /** - * handler to set the workpad zoom level to a specific value - */ - setZoomScale: (scale: number) => void; - /** - * handler to increase the workpad zoom level - */ - zoomIn: () => void; - /** - * handler to decrease workpad zoom level - */ - zoomOut: () => void; - /** - * reset zoom to 100% - */ - resetZoom: () => void; -} - -const QUICK_ZOOM_LEVELS = [0.5, 1, 2]; - -export class WorkpadZoom extends PureComponent { - static propTypes = { - zoomScale: PropTypes.number.isRequired, - setZoomScale: PropTypes.func.isRequired, - zoomIn: PropTypes.func.isRequired, - zoomOut: PropTypes.func.isRequired, - resetZoom: PropTypes.func.isRequired, - boundingBox: PropTypes.shape({ - left: PropTypes.number.isRequired, - right: PropTypes.number.isRequired, - top: PropTypes.number.isRequired, - bottom: PropTypes.number.isRequired, - }), - workpadWidth: PropTypes.number.isRequired, - workpadHeight: PropTypes.number.isRequired, - }; - - _fitToWindow = () => { - const { boundingBox, setZoomScale, workpadWidth, workpadHeight } = this.props; - const canvasLayoutContent = document.querySelector( - `#${CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}` - ) as HTMLElement; - const layoutWidth = canvasLayoutContent.clientWidth; - const layoutHeight = canvasLayoutContent.clientHeight; - const offsetLeft = boundingBox.left; - const offsetTop = boundingBox.top; - const offsetRight = boundingBox.right - workpadWidth; - const offsetBottom = boundingBox.bottom - workpadHeight; - const boundingWidth = - workpadWidth + - Math.max(Math.abs(offsetLeft), Math.abs(offsetRight)) * 2 + - WORKPAD_CANVAS_BUFFER; - const boundingHeight = - workpadHeight + - Math.max(Math.abs(offsetTop), Math.abs(offsetBottom)) * 2 + - WORKPAD_CANVAS_BUFFER; - const xScale = layoutWidth / boundingWidth; - const yScale = layoutHeight / boundingHeight; - - setZoomScale(Math.min(xScale, yScale)); - }; - - _button = (togglePopover: MouseEventHandler) => ( - - ); - - _getScaleMenuItems = (): EuiContextMenuPanelItemDescriptor[] => - QUICK_ZOOM_LEVELS.map(scale => ({ - name: strings.getZoomPercentage(scale), - icon: 'empty', - onClick: () => this.props.setZoomScale(scale), - })); - - _getPanels = (): EuiContextMenuPanelDescriptor[] => { - const { zoomScale, zoomIn, zoomOut, resetZoom } = this.props; - const items: EuiContextMenuPanelItemDescriptor[] = [ - { - name: strings.getZoomFitToWindowText(), - icon: 'empty', - onClick: this._fitToWindow, - disabled: zoomScale === MAX_ZOOM_LEVEL, - }, - ...this._getScaleMenuItems(), - { - name: strings.getZoomInText(), - icon: 'magnifyWithPlus', - onClick: zoomIn, - disabled: zoomScale === MAX_ZOOM_LEVEL, - className: 'canvasContextMenu--topBorder', - }, - { - name: strings.getZoomOutText(), - icon: 'magnifyWithMinus', - onClick: zoomOut, - disabled: zoomScale <= MIN_ZOOM_LEVEL, - }, - { - name: strings.getZoomResetText(), - icon: 'empty', - onClick: resetZoom, - disabled: zoomScale >= MAX_ZOOM_LEVEL, - className: 'canvasContextMenu--topBorder', - }, - ]; - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: strings.getZoomPanelTitle(), - items, - }, - ]; - - return panels; - }; - - render() { - return ( - - {() => } - - ); - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js index 429d27afb3f0d..9379379e54d97 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js @@ -6,14 +6,15 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { compose, withState, getContext, withHandlers } from 'recompose'; +import { compose, withState, getContext, withHandlers, withProps } from 'recompose'; +import moment from 'moment'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; import { canUserWrite } from '../../state/selectors/app'; import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; import { downloadWorkpad } from '../../lib/download_workpad'; import { ComponentStrings, ErrorStrings } from '../../../i18n'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { WorkpadLoader as Component } from './workpad_loader'; const { WorkpadLoader: strings } = ComponentStrings; @@ -30,7 +31,11 @@ export const WorkpadLoader = compose( }), connect(mapStateToProps), withState('workpads', 'setWorkpads', null), - withHandlers({ + withKibana, + withProps(({ kibana }) => ({ + notify: kibana.services.canvas.notify, + })), + withHandlers(({ kibana }) => ({ // Workpad creation via navigation createWorkpad: props => async workpad => { // workpad data uploaded, create and load it @@ -39,7 +44,9 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: errors.getUploadFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); } return; } @@ -53,7 +60,7 @@ export const WorkpadLoader = compose( const workpads = await workpadService.find(text); setWorkpads(workpads); } catch (err) { - notify.error(err, { title: errors.getFindFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { title: errors.getFindFailureErrorMessage() }); } }, @@ -69,7 +76,7 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: errors.getCloneFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); } }, @@ -90,7 +97,7 @@ export const WorkpadLoader = compose( return Promise.all(removeWorkpads).then(results => { let redirectHome = false; - const [passes, errors] = results.reduce( + const [passes, errored] = results.reduce( ([passes, errors], result) => { if (result.id === loadedWorkpad && !result.err) { redirectHome = true; @@ -114,8 +121,8 @@ export const WorkpadLoader = compose( workpads: remainingWorkpads, }; - if (errors.length > 0) { - notify.error(errors.getDeleteFailureErrorMessage()); + if (errored.length > 0) { + kibana.services.canvas.notify.error(errors.getDeleteFailureErrorMessage()); } setWorkpads(workpadState); @@ -124,8 +131,14 @@ export const WorkpadLoader = compose( props.router.navigateTo('home'); } - return errors.map(({ id }) => id); + return errored.map(({ id }) => id); }); }, - }) + })), + withProps(props => ({ + formatDate: date => { + const dateFormat = props.kibana.services.uiSettings.get('dateFormat'); + return date && moment(date).format(dateFormat); + }, + })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js index a7fcf7449ce40..fd25fb03a9ca9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js @@ -6,12 +6,11 @@ import { get } from 'lodash'; import { getId } from '../../lib/get_id'; -import { notify } from '../../lib/notify'; import { ErrorStrings } from '../../../i18n'; const { WorkpadFileUpload: errors } = ErrorStrings; -export const uploadWorkpad = (file, onUpload) => { +export const uploadWorkpad = (file, onUpload, notify) => { if (!file) { return; } diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js index ac716d37f532d..ab0c064d5ef07 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js @@ -6,7 +6,6 @@ import PropTypes from 'prop-types'; import { compose, withHandlers } from 'recompose'; -import { notify } from '../../../lib/notify'; import { uploadWorkpad } from '../upload_workpad'; import { ErrorStrings } from '../../../../i18n'; import { WorkpadDropzone as Component } from './workpad_dropzone'; @@ -14,7 +13,7 @@ import { WorkpadDropzone as Component } from './workpad_dropzone'; const { WorkpadFileUpload: errors } = ErrorStrings; export const WorkpadDropzone = compose( - withHandlers({ + withHandlers(({ notify }) => ({ onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload), onDropRejected: () => ([file]) => { notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { @@ -23,7 +22,7 @@ export const WorkpadDropzone = compose( : errors.getFileUploadFailureWithoutFileNameErrorMessage(), }); }, - }) + })) )(Component); WorkpadDropzone.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 30d4ded8571c5..cb5af27144c7f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -20,12 +20,10 @@ import { EuiLink, } from '@elastic/eui'; import { sortByOrder } from 'lodash'; -import moment from 'moment'; import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { Paginate } from '../paginate'; import { ComponentStrings } from '../../../i18n'; -import { getAdvancedSettings } from '../../lib/kibana_advanced_settings'; import { WorkpadDropzone } from './workpad_dropzone'; import { WorkpadCreate } from './workpad_create'; import { WorkpadSearch } from './workpad_search'; @@ -33,8 +31,6 @@ import { uploadWorkpad } from './upload_workpad'; const { WorkpadLoader: strings } = ComponentStrings; -const formatDate = date => date && moment(date).format(getAdvancedSettings().get('dateFormat')); - const getDisplayName = (name, workpad, loadedWorkpad) => { const workpadName = name.length ? name : {workpad.id}; return workpad.id === loadedWorkpad ? {workpadName} : workpadName; @@ -51,6 +47,7 @@ export class WorkpadLoader extends React.PureComponent { removeWorkpads: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, workpads: PropTypes.object, + formatDate: PropTypes.func.isRequired, }; state = { @@ -205,7 +202,7 @@ export class WorkpadLoader extends React.PureComponent { sortable: true, dataType: 'date', width: '20%', - render: date => formatDate(date), + render: date => this.props.formatDate(date), }, { field: '@timestamp', @@ -213,7 +210,7 @@ export class WorkpadLoader extends React.PureComponent { sortable: true, dataType: 'date', width: '20%', - render: date => formatDate(date), + render: date => this.props.formatDate(date), }, { name: '', actions, width: '5%' }, ]; @@ -252,7 +249,11 @@ export class WorkpadLoader extends React.PureComponent { return ( - + uploadWorkpad(file, this.onUpload)} + onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)} accept="application/json" disabled={createPending || !canUserWrite} /> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js index 139d0f283bf1a..1890ca1f9d2d6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js @@ -7,9 +7,9 @@ import PropTypes from 'prop-types'; import { compose, getContext, withHandlers, withProps } from 'recompose'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; import { getId } from '../../lib/get_id'; import { templatesRegistry } from '../../lib/templates_registry'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { WorkpadTemplates as Component } from './workpad_templates'; export const WorkpadTemplates = compose( @@ -19,7 +19,8 @@ export const WorkpadTemplates = compose( withProps(() => ({ templates: templatesRegistry.toJS(), })), - withHandlers({ + withKibana, + withHandlers(({ kibana }) => ({ // Clone workpad given an id cloneWorkpad: props => workpad => { workpad.id = getId('workpad'); @@ -31,7 +32,9 @@ export const WorkpadTemplates = compose( return workpadService .create(workpad) .then(() => props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 })) - .catch(err => notify.error(err, { title: `Couldn't clone workpad template` })); + .catch(err => + kibana.services.canvas.notify.error(err, { title: `Couldn't clone workpad template` }) + ); }, - }) + })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/functions/index.ts b/x-pack/legacy/plugins/canvas/public/functions/index.ts index 27fb7d83274a4..5e098d8f175c5 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/index.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/index.ts @@ -4,16 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { asset } from './asset'; import { filtersFunctionFactory } from './filters'; -import { timelion } from './timelion'; +import { timelionFunctionFactory } from './timelion'; import { toFunctionFactory } from './to'; +import { CanvasSetupDeps, CoreSetup } from '../plugin'; export interface InitializeArguments { - typesRegistry: ExpressionsSetup['__LEGACY']['types']; + prependBasePath: CoreSetup['http']['basePath']['prepend']; + typesRegistry: CanvasSetupDeps['expressions']['__LEGACY']['types']; + timefilter: CanvasSetupDeps['data']['query']['timefilter']['timefilter']; } export function initFunctions(initialize: InitializeArguments) { - return [asset, filtersFunctionFactory(initialize), timelion, toFunctionFactory(initialize)]; + return [ + asset, + filtersFunctionFactory(initialize), + timelionFunctionFactory(initialize), + toFunctionFactory(initialize), + ]; } diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index ae87e858cf796..e59d798108945 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -6,8 +6,6 @@ import { flatten } from 'lodash'; import moment from 'moment-timezone'; -import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; @@ -15,6 +13,7 @@ import { fetch } from '../../common/lib/fetch'; import { buildBoolArray } from '../../server/lib/build_bool_array'; import { Datatable, Filter } from '../../types'; import { getFunctionHelp } from '../../i18n'; +import { InitializeArguments } from './'; interface Arguments { query: string; @@ -30,13 +29,17 @@ interface Arguments { * @param timeRange time range to parse * @param timeZone time zone to do the parsing in */ -function parseDateMath(timeRange: TimeRange, timeZone: string) { +function parseDateMath( + timeRange: TimeRange, + timeZone: string, + timefilter: InitializeArguments['timefilter'] +) { // the datemath plugin always parses dates by using the current default moment time zone. // to use the configured time zone, we are switching just for the bounds calculation. const defaultTimezone = moment().zoneName(); moment.tz.setDefault(timeZone); - const parsedRange = npStart.plugins.data.query.timefilter.timefilter.calculateBounds(timeRange); + const parsedRange = timefilter.calculateBounds(timeRange); // reset default moment timezone moment.tz.setDefault(defaultTimezone); @@ -44,96 +47,100 @@ function parseDateMath(timeRange: TimeRange, timeZone: string) { return parsedRange; } -export function timelion(): ExpressionFunctionDefinition< +type TimelionFunction = ExpressionFunctionDefinition< 'timelion', Filter, Arguments, Promise -> { - const { help, args: argHelp } = getFunctionHelp().timelion; +>; - return { - name: 'timelion', - type: 'datatable', - inputTypes: ['filter'], - help, - args: { - query: { - types: ['string'], - aliases: ['_', 'q'], - help: argHelp.query, - default: '".es(*)"', - }, - interval: { - types: ['string'], - help: argHelp.interval, - default: 'auto', - }, - from: { - types: ['string'], - help: argHelp.from, - default: 'now-1y', - }, - to: { - types: ['string'], - help: argHelp.to, - default: 'now', - }, - timezone: { - types: ['string'], - help: argHelp.timezone, - default: 'UTC', +export function timelionFunctionFactory(initialize: InitializeArguments): () => TimelionFunction { + return () => { + const { help, args: argHelp } = getFunctionHelp().timelion; + + return { + name: 'timelion', + type: 'datatable', + inputTypes: ['filter'], + help, + args: { + query: { + types: ['string'], + aliases: ['_', 'q'], + help: argHelp.query, + default: '".es(*)"', + }, + interval: { + types: ['string'], + help: argHelp.interval, + default: 'auto', + }, + from: { + types: ['string'], + help: argHelp.from, + default: 'now-1y', + }, + to: { + types: ['string'], + help: argHelp.to, + default: 'now', + }, + timezone: { + types: ['string'], + help: argHelp.timezone, + default: 'UTC', + }, }, - }, - fn: (input, args): Promise => { - // Timelion requires a time range. Use the time range from the timefilter element in the - // workpad, if it exists. Otherwise fall back on the function args. - const timeFilter = input.and.find(and => and.type === 'time'); - const range = timeFilter - ? { min: timeFilter.from, max: timeFilter.to } - : parseDateMath({ from: args.from, to: args.to }, args.timezone); + fn: (input, args): Promise => { + // Timelion requires a time range. Use the time range from the timefilter element in the + // workpad, if it exists. Otherwise fall back on the function args. + const timeFilter = input.and.find(and => and.type === 'time'); + const range = timeFilter + ? { min: timeFilter.from, max: timeFilter.to } + : parseDateMath({ from: args.from, to: args.to }, args.timezone, initialize.timefilter); - const body = { - extended: { - es: { - filter: { - bool: { - must: buildBoolArray(input.and), + const body = { + extended: { + es: { + filter: { + bool: { + must: buildBoolArray(input.and), + }, }, }, }, - }, - sheet: [args.query], - time: { - from: range.min, - to: range.max, - interval: args.interval, - timezone: args.timezone, - }, - }; + sheet: [args.query], + time: { + from: range.min, + to: range.max, + interval: args.interval, + timezone: args.timezone, + }, + }; - return fetch(chrome.addBasePath(`/api/timelion/run`), { - method: 'POST', - responseType: 'json', - data: body, - }).then(resp => { - const seriesList = resp.data.sheet[0].list; - const rows = flatten( - seriesList.map((series: { data: any[]; label: string }) => - series.data.map(row => ({ '@timestamp': row[0], value: row[1], label: series.label })) - ) - ) as DatatableRow[]; + return fetch(initialize.prependBasePath(`/api/timelion/run`), { + method: 'POST', + responseType: 'json', + data: body, + }).then(resp => { + const seriesList = resp.data.sheet[0].list; + const rows = flatten( + seriesList.map((series: { data: any[]; label: string }) => + series.data.map(row => ({ '@timestamp': row[0], value: row[1], label: series.label })) + ) + ) as DatatableRow[]; - return { - type: 'datatable', - columns: [ - { name: '@timestamp', type: 'date' }, - { name: 'value', type: 'number' }, - { name: 'label', type: 'string' }, - ], - rows, - }; - }); - }, + return { + type: 'datatable', + columns: [ + { name: '@timestamp', type: 'date' }, + { name: 'value', type: 'number' }, + { name: 'label', type: 'string' }, + ], + rows, + }; + }); + }, + }; }; } diff --git a/x-pack/legacy/plugins/canvas/public/index.ts b/x-pack/legacy/plugins/canvas/public/index.ts index b8358bfe022e6..b053920fec6e4 100644 --- a/x-pack/legacy/plugins/canvas/public/index.ts +++ b/x-pack/legacy/plugins/canvas/public/index.ts @@ -10,6 +10,7 @@ import { CoreStart, } from '../../../../../src/core/public'; import { CanvasSetup, CanvasStart, CanvasSetupDeps, CanvasStartDeps, CanvasPlugin } from './plugin'; +import { CanvasServices } from './services'; export const plugin: PluginInitializer< CanvasSetup, @@ -22,7 +23,7 @@ export const plugin: PluginInitializer< export interface WithKibanaProps { kibana: { - services: CoreStart & CanvasStartDeps; + services: CoreStart & CanvasStartDeps & { canvas: CanvasServices }; }; } diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index 4af7c9b2bd057..5bb628909c32e 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -21,8 +21,10 @@ const shimCoreStart = { }; const shimSetupPlugins: CanvasSetupDeps = { + data: npSetup.plugins.data, expressions: npSetup.plugins.expressions, home: npSetup.plugins.home, + usageCollection: npSetup.plugins.usageCollection, }; const shimStartPlugins: CanvasStartDeps = { ...npStart.plugins, diff --git a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts index 478e2f8f18cf5..4118bb81b8363 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts @@ -5,9 +5,7 @@ */ import { AxiosPromise } from 'axios'; -// @ts-ignore unconverted local file import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; -// @ts-ignore unconverted local file import { fetch } from '../../common/lib/fetch'; import { CustomElement } from '../../types'; import { getCoreStart } from '../legacy'; @@ -25,7 +23,7 @@ export const get = (customElementId: string): Promise => .get(`${getApiPath()}/${customElementId}`) .then(({ data: element }: { data: CustomElement }) => element); -export const update = (id: string, element: CustomElement): AxiosPromise => +export const update = (id: string, element: Partial): AxiosPromise => fetch.put(`${getApiPath()}/${id}`, element); export const remove = (id: string): AxiosPromise => fetch.delete(`${getApiPath()}/${id}`); diff --git a/x-pack/legacy/plugins/canvas/public/lib/default_header.png b/x-pack/legacy/plugins/canvas/public/lib/default_header.png deleted file mode 100644 index 0b5c5b8f58f9b..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/public/lib/default_header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts index e4866641fd9e1..fb038d8b6ace2 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts @@ -6,8 +6,7 @@ import fileSaver from 'file-saver'; import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants'; import { ErrorStrings } from '../../i18n'; -// @ts-ignore untyped local -import { notify } from './notify'; +import { notifyService } from '../services'; // @ts-ignore untyped local import * as workpadService from './workpad_service'; import { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; @@ -20,7 +19,7 @@ export const downloadWorkpad = async (workpadId: string) => { const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' }); fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`); } catch (err) { - notify.error(err, { title: strings.getDownloadFailureErrorMessage() }); + notifyService.getService().error(err, { title: strings.getDownloadFailureErrorMessage() }); } }; @@ -32,7 +31,9 @@ export const downloadRenderedWorkpad = async (renderedWorkpad: CanvasRenderedWor `canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json` ); } catch (err) { - notify.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); } }; @@ -42,7 +43,9 @@ export const downloadRuntime = async (basePath: string) => { window.open(path); return; } catch (err) { - notify.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); } }; @@ -51,6 +54,8 @@ export const downloadZippedRuntime = async (data: any) => { const zip = new Blob([data], { type: 'octet/stream' }); fileSaver.saveAs(zip, 'canvas-workpad-embed.zip'); } catch (err) { - notify.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); } }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/element.ts b/x-pack/legacy/plugins/canvas/public/lib/element.ts index 121c253668ed9..ef1cf601b6e26 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/element.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/element.ts @@ -5,18 +5,16 @@ */ import { ElementSpec } from '../../types'; -import defaultHeader from './default_header.png'; -import { tagsRegistry } from './tags_registry'; export class Element { /** The name of the Element. This must match the name of the function that is used to create the `type: render` object */ public name: string; /** A more friendly name for the Element */ public displayName: string; - /** Relevant labels to help identify the elements */ - public tags: string[]; - /** An image to use in the Element type selector */ - public image: string; + /** The type of the Element */ + public type?: string; + /** The name of the EUI icon to display in the element menu */ + public icon: string; /** A sentence or few about what this Element does */ public help: string; /** A default expression that allows Canvas to render the Element */ @@ -28,23 +26,17 @@ export class Element { public height?: number; constructor(config: ElementSpec) { - const { name, image, displayName, tags, expression, filter, help, width, height } = config; + const { name, icon, displayName, type, expression, filter, help, width, height } = config; this.name = name; this.displayName = displayName || name; - this.image = image || defaultHeader; + this.icon = icon || 'empty'; this.help = help || ''; if (!config.expression) { throw new Error('Element types must have a default expression'); } - this.tags = tags || []; - - this.tags.forEach(tag => { - if (!tagsRegistry.get(tag)) { - tagsRegistry.register(() => ({ name: tag, color: '#666666' })); - } - }); + this.type = type; this.expression = expression; this.filter = filter; this.width = width || 500; diff --git a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts index bce6bc51b366c..a8744b4820842 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Http2ServerResponse } from 'http2'; import { camelCase } from 'lodash'; // @ts-ignore unconverted local file import { getClipboardData, setClipboardData } from './clipboard'; // @ts-ignore unconverted local file import { cloneSubgraphs } from './clone_subgraphs'; -// @ts-ignore unconverted local file -import { notify } from './notify'; +import { notifyService } from '../services'; import * as customElementService from './custom_element_service'; import { getId } from './get_id'; import { PositionedElement } from '../../types'; @@ -86,15 +84,17 @@ export const basicHandlerCreators = { customElementService .create(customElement) .then(() => - notify.success( - `Custom element '${customElement.displayName || customElement.id}' was saved`, - { - 'data-test-subj': 'canvasCustomElementCreate-success', - } - ) + notifyService + .getService() + .success( + `Custom element '${customElement.displayName || customElement.id}' was saved`, + { + 'data-test-subj': 'canvasCustomElementCreate-success', + } + ) ) - .catch((result: Http2ServerResponse) => - notify.warning(result, { + .catch((error: Error) => + notifyService.getService().warning(error, { title: `Custom element '${customElement.displayName || customElement.id}' was not saved`, }) @@ -138,13 +138,13 @@ export const clipboardHandlerCreators = { if (selectedNodes.length) { setClipboardData({ selectedNodes }); removeNodes(selectedNodes.map(extractId), pageId); - notify.success('Cut element to clipboard'); + notifyService.getService().success('Cut element to clipboard'); } }, copyNodes: ({ selectedNodes }: Props) => (): void => { if (selectedNodes.length) { setClipboardData({ selectedNodes }); - notify.success('Copied element to clipboard'); + notifyService.getService().success('Copied element to clipboard'); } }, pasteNodes: ({ insertNodes, pageId, selectToplevelNodes }: Props) => (): void => { diff --git a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts index 32f4fe041423c..6aa4968f29155 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts @@ -10,8 +10,7 @@ import { API_ROUTE } from '../../common/lib/constants'; // @ts-ignore untyped local import { fetch } from '../../common/lib/fetch'; import { ErrorStrings } from '../../i18n'; -// @ts-ignore untyped local -import { notify } from './notify'; +import { notifyService } from '../services'; import { getCoreStart } from '../legacy'; const { esService: strings } = ErrorStrings; @@ -38,7 +37,7 @@ export const getFields = (index = '_all') => { .sort() ) .catch((err: Error) => - notify.error(err, { + notifyService.getService().error(err, { title: strings.getFieldsFetchErrorMessage(index), }) ); @@ -57,7 +56,9 @@ export const getIndices = () => return savedObject.attributes.title; }); }) - .catch((err: Error) => notify.error(err, { title: strings.getIndicesFetchErrorMessage() })); + .catch((err: Error) => + notifyService.getService().error(err, { title: strings.getIndicesFetchErrorMessage() }) + ); export const getDefaultIndex = () => { const defaultIndexId = getAdvancedSettings().get('defaultIndex'); @@ -66,6 +67,10 @@ export const getDefaultIndex = () => { ? getSavedObjectsClient() .get('index-pattern', defaultIndexId) .then(defaultIndex => defaultIndex.attributes.title) - .catch(err => notify.error(err, { title: strings.getDefaultIndexFetchErrorMessage() })) + .catch(err => + notifyService + .getService() + .error(err, { title: strings.getDefaultIndexFetchErrorMessage() }) + ) : Promise.resolve(''); }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts b/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts new file mode 100644 index 0000000000000..a059d07725727 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +// TODO: Fix all of this magic from EUI; this code is boilerplate from +// EUI examples and isn't easily typed. +export const flattenPanelTree = (tree: any, array: any[] = []) => { + array.push(tree); + + if (tree.items) { + tree.items.forEach((item: any) => { + const { panel } = item; + if (panel) { + flattenPanelTree(panel, array); + item.panel = panel.id; + } + }); + } + + return array; +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts b/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts deleted file mode 100644 index f57f3188a8184..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts +++ /dev/null @@ -1,9 +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 { getCoreStart } from '../legacy'; - -export const getAdvancedSettings = () => getCoreStart().uiSettings; diff --git a/x-pack/legacy/plugins/canvas/public/lib/notify.js b/x-pack/legacy/plugins/canvas/public/lib/notify.js deleted file mode 100644 index 64876a02a3c64..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/notify.js +++ /dev/null @@ -1,52 +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 { get } from 'lodash'; -import { getCoreStart, getStartPlugins } from '../legacy'; - -const getToastNotifications = function() { - return getCoreStart().notifications.toasts; -}; - -const formatMsg = function(...args) { - return getStartPlugins().__LEGACY.formatMsg(...args); -}; - -const getToast = (err, opts = {}) => { - const errData = get(err, 'response') || err; - const errMsg = formatMsg(errData); - const { title, ...rest } = opts; - let text = null; - - if (title) { - text = errMsg; - } - - return { - ...rest, - title: title || errMsg, - text, - }; -}; - -export const notify = { - /* - * @param {(string | Object)} err: message or Error object - * @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md - */ - error(err, opts) { - getToastNotifications().addDanger(getToast(err, opts)); - }, - warning(err, opts) { - getToastNotifications().addWarning(getToast(err, opts)); - }, - info(err, opts) { - getToastNotifications().add(getToast(err, opts)); - }, - success(err, opts) { - getToastNotifications().addSuccess(getToast(err, opts)); - }, -}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts index fbbaf0ccf280e..df338f40e08d9 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts @@ -6,8 +6,7 @@ import { fromExpression, getType } from '@kbn/interpreter/common'; import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; -// @ts-ignore Untyped Local -import { notify } from './notify'; +import { notifyService } from '../services'; import { CanvasStartDeps, CanvasSetupDeps } from '../plugin'; @@ -85,7 +84,7 @@ export async function runInterpreter( throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); } catch (err) { - notify.error(err); + notifyService.getService().error(err); throw err; } } diff --git a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts index 33976a147df46..2a1a4b88b7264 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts @@ -4,10 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -export const trackCanvasUiMetric = createUiStatsReporter('canvas'); export { METRIC_TYPE }; + +export let reportUiStats: UsageCollectionSetup['reportUiStats'] | undefined; + +export function init(_reportUiStats: UsageCollectionSetup['reportUiStats']): void { + reportUiStats = _reportUiStats; +} + +export function trackCanvasUiMetric(metricType: UiStatsMetricType, name: string | string[]) { + if (!reportUiStats) { + return; + } + + reportUiStats('canvas', metricType, name); +} diff --git a/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js b/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js index bf97928452c71..75307816b4371 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js +++ b/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js @@ -47,9 +47,16 @@ window.canvasInitErrorHandler = () => { window.onerror = (...args) => { const [message, , , , err] = args; - const isKnownError = Object.keys(knownErrors).find(errorName => { - return err.constructor.name === errorName || message.indexOf(errorName) >= 0; - }); + // ResizeObserver error does not have an `err` object + // It is thrown during workpad loading due to layout thrashing + // https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded + // https://github.com/elastic/eui/issues/3346 + console.log(message); + const isKnownError = + message.includes('ResizeObserver loop') || + Object.keys(knownErrors).find(errorName => { + return err.constructor.name === errorName || message.indexOf(errorName) >= 0; + }); if (isKnownError) { return; } diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 3ea3ce625ca71..36ce1974be272 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -10,8 +10,10 @@ import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { initLoadingIndicator } from './lib/loading_indicator'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; // @ts-ignore untyped local import { argTypeSpecs } from './expression_types/arg_types'; @@ -20,7 +22,7 @@ import { legacyRegistries } from './legacy_plugin_support'; import { getPluginApi, CanvasApi } from './plugin_api'; import { initFunctions } from './functions'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; -export { CoreStart }; +export { CoreStart, CoreSetup }; /** * These are the private interfaces for the services your plugin depends on. @@ -28,14 +30,17 @@ export { CoreStart }; */ // This interface will be built out as we require other plugins for setup export interface CanvasSetupDeps { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; home: HomePublicPluginSetup; + usageCollection?: UsageCollectionSetup; } export interface CanvasStartDeps { embeddable: EmbeddableStart; expressions: ExpressionsStart; inspector: InspectorStart; + uiActions: UiActionsStart; __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; @@ -94,7 +99,13 @@ export class CanvasPlugin canvasApi.addTypes(legacyRegistries.types.getOriginalFns()); // Register core canvas stuff - canvasApi.addFunctions(initFunctions({ typesRegistry: plugins.expressions.__LEGACY.types })); + canvasApi.addFunctions( + initFunctions({ + timefilter: plugins.data.query.timefilter.timefilter, + prependBasePath: core.http.basePath.prepend, + typesRegistry: plugins.expressions.__LEGACY.types, + }) + ); canvasApi.addArgumentUIs(argTypeSpecs); canvasApi.addTransitions(transitions); diff --git a/x-pack/legacy/plugins/canvas/public/services/index.ts b/x-pack/legacy/plugins/canvas/public/services/index.ts new file mode 100644 index 0000000000000..12c0a687bf308 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/index.ts @@ -0,0 +1,73 @@ +/* + * 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 { CoreSetup, CoreStart } from '../../../../../../src/core/public'; +import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; +import { notifyServiceFactory } from './notify'; + +export type CanvasServiceFactory = ( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps +) => Service; + +class CanvasServiceProvider { + private factory: CanvasServiceFactory; + private service: Service | undefined; + + constructor(factory: CanvasServiceFactory) { + this.factory = factory; + } + + start( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps + ) { + this.service = this.factory(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins); + } + + getService(): Service { + if (!this.service) { + throw new Error('Service not ready'); + } + + return this.service; + } + + stop() { + this.service = undefined; + } +} + +export type ServiceFromProvider

= P extends CanvasServiceProvider ? T : never; + +export const services = { + notify: new CanvasServiceProvider(notifyServiceFactory), +}; + +export interface CanvasServices { + notify: ServiceFromProvider; +} + +export const startServices = ( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps +) => { + Object.entries(services).forEach(([key, provider]) => + provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins) + ); +}; + +export const stopServices = () => { + Object.entries(services).forEach(([key, provider]) => provider.stop()); +}; + +export const { notify: notifyService } = services; diff --git a/x-pack/legacy/plugins/canvas/public/services/notify.ts b/x-pack/legacy/plugins/canvas/public/services/notify.ts new file mode 100644 index 0000000000000..3e18e2178a818 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/notify.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { CanvasServiceFactory } from '.'; +import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; +import { ToastInputFields } from '../../../../../../src/core/public'; + +const getToast = (err: Error | string, opts: ToastInputFields = {}) => { + const errData = (get(err, 'response') || err) as Error | string; + const errMsg = formatMsg(errData); + const { title, ...rest } = opts; + let text; + + if (title) { + text = errMsg; + } + + return { + ...rest, + title: title || errMsg, + text, + }; +}; + +interface NotifyService { + error: (err: string | Error, opts?: ToastInputFields) => void; + warning: (err: string | Error, opts?: ToastInputFields) => void; + info: (err: string | Error, opts?: ToastInputFields) => void; + success: (err: string | Error, opts?: ToastInputFields) => void; +} + +export const notifyServiceFactory: CanvasServiceFactory = (setup, start) => { + const toasts = start.notifications.toasts; + + return { + /* + * @param {(string | Object)} err: message or Error object + * @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md + */ + error(err, opts) { + toasts.addDanger(getToast(err, opts)); + }, + warning(err, opts) { + toasts.addWarning(getToast(err, opts)); + }, + info(err, opts) { + toasts.add(getToast(err, opts)); + }, + success(err, opts) { + toasts.addSuccess(getToast(err, opts)); + }, + }; +}; diff --git a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js index 1798aaab22f06..f4a3393b8962d 100644 --- a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js @@ -13,9 +13,9 @@ import { getPages, getNodeById, getNodes, getSelectedPageIndex } from '../select import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; import { ErrorStrings } from '../../../i18n'; -import { notify } from '../../lib/notify'; import { runInterpreter, interpretAst } from '../../lib/run_interpreter'; import { subMultitree } from '../../lib/aeroelastic/functional'; +import { services } from '../../services'; import { selectToplevelNodes } from './transient'; import * as args from './resolved_args'; @@ -134,7 +134,7 @@ const fetchRenderableWithContextFn = ({ dispatch }, element, ast, context) => { dispatch(getAction(renderable)); }) .catch(err => { - notify.error(err); + services.notify.getService().error(err); dispatch(getAction(err)); }); }; @@ -176,7 +176,7 @@ export const fetchAllRenderables = createThunk( return runInterpreter(ast, null, { castToRender: true }) .then(renderable => ({ path: argumentPath, value: renderable })) .catch(err => { - notify.error(err); + services.notify.getService().error(err); return { path: argumentPath, value: err }; }); }); @@ -293,7 +293,7 @@ const setAst = createThunk('setAst', ({ dispatch }, ast, element, pageId, doRend const expression = toExpression(ast); dispatch(setExpression(expression, element.id, pageId, doRender)); } catch (err) { - notify.error(err); + services.notify.getService().error(err); // TODO: remove this, may have been added just to cause a re-render, but why? dispatch(setExpression(element.expression, element.id, pageId, doRender)); diff --git a/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js b/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js index bcbfc3544981a..a197cdf893244 100644 --- a/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js +++ b/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js @@ -14,7 +14,7 @@ import { setAssets, resetAssets } from '../actions/assets'; import * as transientActions from '../actions/transient'; import * as resolvedArgsActions from '../actions/resolved_args'; import { update, updateAssets, updateWorkpad } from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; +import { services } from '../../services'; import { canUserWrite } from '../selectors/app'; const { esPersist: strings } = ErrorStrings; @@ -62,15 +62,15 @@ export const esPersistMiddleware = ({ getState }) => { const statusCode = err.response && err.response.status; switch (statusCode) { case 400: - return notify.error(err.response, { + return services.notify.getService().error(err.response, { title: strings.getSaveFailureTitle(), }); case 413: - return notify.error(strings.getTooLargeErrorMessage(), { + return services.notify.getService().error(strings.getTooLargeErrorMessage(), { title: strings.getSaveFailureTitle(), }); default: - return notify.error(err, { + return services.notify.getService().error(err, { title: strings.getUpdateFailureTitle(), }); } diff --git a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts index 84fab0cb0ae6d..1623035bd25ba 100644 --- a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts @@ -11,7 +11,12 @@ import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/comm import { append } from '../../lib/modify_path'; import { getAssets } from './assets'; import { State, CanvasWorkpad, CanvasPage, CanvasElement, ResolvedArgType } from '../../../types'; -import { ExpressionContext, CanvasGroup, PositionedElement } from '../../../types'; +import { + ExpressionContext, + CanvasGroup, + PositionedElement, + CanvasWorkpadBoundingBox, +} from '../../../types'; import { ExpressionAstArgument, ExpressionAstFunction, @@ -91,7 +96,7 @@ export function getWorkpadWidth(state: State): number { return get(state, append(workpadRoot, 'width')); } -export function getWorkpadBoundingBox(state: State) { +export function getWorkpadBoundingBox(state: State): CanvasWorkpadBoundingBox { return getPages(state).reduce( (boundingBox, page) => { page.elements.forEach(({ position }) => { diff --git a/x-pack/legacy/plugins/canvas/public/style/index.scss b/x-pack/legacy/plugins/canvas/public/style/index.scss index 56f9ed8d18cbe..ba0845862368a 100644 --- a/x-pack/legacy/plugins/canvas/public/style/index.scss +++ b/x-pack/legacy/plugins/canvas/public/style/index.scss @@ -52,7 +52,8 @@ @import '../components/tooltip_annotation/tooltip_annotation'; @import '../components/workpad/workpad'; @import '../components/workpad_header/control_settings/control_settings'; -@import '../components/workpad_header/workpad_export/workpad_export'; +@import '../components/workpad_header/element_menu/element_menu'; +@import '../components/workpad_header/share_menu/share_menu'; @import '../components/workpad_loader/workpad_loader'; @import '../components/workpad_loader/workpad_dropzone/workpad_dropzone'; @import '../components/workpad_page/workpad_page'; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts index b05379df6b0b1..0780ab46cd873 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import 'core-js/stable'; +import 'regenerator-runtime/runtime'; import 'whatwg-fetch'; -import 'babel-polyfill'; export * from './shareable'; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json b/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json index f9f999440c7ce..b725afab2b10f 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json @@ -28878,7 +28878,7 @@ "type": "render", "as": "markdown", "value": { - "content": "```\nexport const githubLimitGauge = () => ({\n name: 'githubLimitGauge',\n displayName: 'Github Limit Gauge',\n help: 'A progress pill displaying...',\n image: header,\n expression: `github_rate_limit \n | filterrows fn={getCell name | eq core} \n | if \n condition={math limit | eq 0} \n then=0 \n else={math \"remaining / limit\"}\n | progress \n label=\"Core\"\n shape=\"horizontalPill\" \n | render\n `,\n };\n}\n```", + "content": "```\nexport const githubLimitGauge = () => ({\n name: 'githubLimitGauge',\n displayName: 'Github Limit Gauge',\n help: 'A progress pill displaying...',\n expression: `github_rate_limit \n | filterrows fn={getCell name | eq core} \n | if \n condition={math limit | eq 0} \n then=0 \n else={math \"remaining / limit\"}\n | progress \n label=\"Core\"\n shape=\"horizontalPill\" \n | render\n `,\n };\n}\n```", "font": { "type": "style", "spec": { @@ -28919,7 +28919,7 @@ "type": "render", "as": "markdown", "value": { - "content": "```\nexport function randomNumber() {\n return {\n name: 'randomNumber',\n displayName: 'Random Number',\n help: 'A random number between 0 and 1.',\n image: header,\n expression: \n 'random \n | math \"round(value, 3)\" \n | metric \"Random Number\"\n ',\n };\n}\n```", + "content": "```\nexport function randomNumber() {\n return {\n name: 'randomNumber',\n displayName: 'Random Number',\n help: 'A random number between 0 and 1.',\n expression: \n 'random \n | math \"round(value, 3)\" \n | metric \"Random Number\"\n ',\n };\n}\n```", "font": { "type": "style", "spec": { diff --git a/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js b/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js new file mode 100644 index 0000000000000..3162638cb6c5d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const testCustomElements = [ + { + id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', + name: 'customElement1', + displayName: 'Custom Element 1', + help: 'sample description', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"element-3383b40a-de5d-4efb-8719-f4d8cffbfa74\",\"position\":{\"left\":142,\"top\":146,\"width\":700,\"height\":300,\"angle\":0,\"parent\":null,\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| pointseries x=\\\"project\\\" y=\\\"sum(price)\\\" color=\\\"state\\\" size=\\\"size(username)\\\"\\n| plot defaultStyle={seriesStyle points=5 fill=1}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"pointseries\",\"arguments\":{\"x\":[\"project\"],\"y\":[\"sum(price)\"],\"color\":[\"state\"],\"size\":[\"size(username)\"]}},{\"type\":\"function\",\"function\":\"plot\",\"arguments\":{\"defaultStyle\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"seriesStyle\",\"arguments\":{\"points\":[5],\"fill\":[1]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-b22d8d10-6116-46fb-9b46-c3f3340d3aaa', + name: 'customElement2', + displayName: 'Custom Element 2', + help: 'Aenean eu justo auctor, placerat felis non, scelerisque dolor. ', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-', + name: 'customElement3', + displayName: 'Custom Element 3', + help: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, +]; + +export const create = () => {}; + +export const get = () => {}; + +export const update = () => {}; + +export const remove = () => {}; + +export const find = () => {}; diff --git a/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js b/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js new file mode 100644 index 0000000000000..c7e7088812148 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js @@ -0,0 +1,7 @@ +/* + * 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 const trackCanvasUiMetric = () => {}; diff --git a/x-pack/legacy/plugins/canvas/types/canvas.ts b/x-pack/legacy/plugins/canvas/types/canvas.ts index f0137479a0b7f..0250b921aadb6 100644 --- a/x-pack/legacy/plugins/canvas/types/canvas.ts +++ b/x-pack/legacy/plugins/canvas/types/canvas.ts @@ -56,3 +56,10 @@ export type CanvasTemplate = CanvasWorkpad & { help: string; tags: string[]; }; + +export interface CanvasWorkpadBoundingBox { + left: number; + right: number; + top: number; + bottom: number; +} diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index acb1cb9cd7625..86356f5bd32a9 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -9,10 +9,10 @@ import { CanvasElement } from '.'; export interface ElementSpec { name: string; - image: string; + icon?: string; expression: string; displayName?: string; - tags?: string[]; + type?: string; help?: string; filter?: string; width?: number; @@ -42,10 +42,6 @@ export interface CustomElement { * base 64 data URL string of the preview image */ image?: string; - /** - * tags associated with the element - */ - tags?: string[]; /** * the element object stringified */ diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js deleted file mode 100644 index 664ad909ba8e7..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js +++ /dev/null @@ -1,22 +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 { registerTestBed } from '../../../../../../test_utils'; -import { CrossClusterReplicationHome } from '../../../public/np_ready/app/sections/home/home'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; -import { BASE_PATH } from '../../../common/constants'; - -const testBedConfig = { - store: ccrStore, - memoryRouter: { - initialEntries: [`${BASE_PATH}/follower_indices`], - componentRoutePath: `${BASE_PATH}/:section`, - onRouter: router => (routing.reactRouter = router), - }, -}; - -export const setup = registerTestBed(CrossClusterReplicationHome, testBedConfig); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts deleted file mode 100644 index 4ce0a2f5644f3..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts +++ /dev/null @@ -1,10 +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. - */ - -export const APPS = { - CCR_APP: 'ccr', - REMOTE_CLUSTER_APP: 'remote_cluster', -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts deleted file mode 100644 index 0a948793e07db..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts +++ /dev/null @@ -1,11 +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. - */ - -export const BASE_PATH = '/management/elasticsearch/cross_cluster_replication'; -export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters'; -export const API_BASE_PATH = '/api/cross_cluster_replication'; -export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters'; -export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts deleted file mode 100644 index 300afb4e2d2ff..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export * from './plugin'; -export * from './base_path'; -export * from './app'; -export * from './settings'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts deleted file mode 100644 index bd5bb50514c01..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts +++ /dev/null @@ -1,9 +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. - */ - -export const PLUGIN = { - ID: 'cross_cluster_replication', -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts deleted file mode 100644 index 0993a74c8f1fd..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts +++ /dev/null @@ -1,18 +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. - */ - -export const FOLLOWER_INDEX_ADVANCED_SETTINGS = { - maxReadRequestOperationCount: 5120, - maxOutstandingReadRequests: 12, - maxReadRequestSize: '32mb', - maxWriteRequestOperationCount: 5120, - maxWriteRequestSize: '9223372036854775807b', - maxOutstandingWriteRequests: 9, - maxWriteBufferCount: 2147483647, - maxWriteBufferSize: '512mb', - maxRetryDelay: '500ms', - readPollTimeout: '1m', -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap deleted file mode 100644 index d001459e8234d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap +++ /dev/null @@ -1,128 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = ` -Object { - "leaderIndex": undefined, - "maxOutstandingReadRequests": undefined, - "maxOutstandingWriteRequests": undefined, - "maxReadRequestOperationCount": undefined, - "maxReadRequestSize": undefined, - "maxRetryDelay": undefined, - "maxWriteBufferCount": undefined, - "maxWriteBufferSize": undefined, - "maxWriteRequestOperationCount": undefined, - "maxWriteRequestSize": undefined, - "name": undefined, - "readPollTimeout": undefined, - "remoteCluster": undefined, - "shards": Array [ - Object { - "bytesReadCount": undefined, - "failedReadRequestsCount": undefined, - "failedWriteRequestsCount": undefined, - "followerGlobalCheckpoint": undefined, - "followerMappingVersion": undefined, - "followerMaxSequenceNum": undefined, - "followerSettingsVersion": undefined, - "id": "shard 1", - "lastRequestedSequenceNum": undefined, - "leaderGlobalCheckpoint": undefined, - "leaderIndex": undefined, - "leaderMaxSequenceNum": undefined, - "operationsReadCount": undefined, - "operationsWrittenCount": undefined, - "outstandingReadRequestsCount": undefined, - "outstandingWriteRequestsCount": undefined, - "readExceptions": undefined, - "remoteCluster": undefined, - "successfulReadRequestCount": undefined, - "successfulWriteRequestsCount": undefined, - "timeSinceLastReadMs": undefined, - "totalReadRemoteExecTimeMs": undefined, - "totalReadTimeMs": undefined, - "totalWriteTimeMs": undefined, - "writeBufferOperationsCount": undefined, - "writeBufferSizeBytes": undefined, - }, - Object { - "bytesReadCount": undefined, - "failedReadRequestsCount": undefined, - "failedWriteRequestsCount": undefined, - "followerGlobalCheckpoint": undefined, - "followerMappingVersion": undefined, - "followerMaxSequenceNum": undefined, - "followerSettingsVersion": undefined, - "id": "shard 2", - "lastRequestedSequenceNum": undefined, - "leaderGlobalCheckpoint": undefined, - "leaderIndex": undefined, - "leaderMaxSequenceNum": undefined, - "operationsReadCount": undefined, - "operationsWrittenCount": undefined, - "outstandingReadRequestsCount": undefined, - "outstandingWriteRequestsCount": undefined, - "readExceptions": undefined, - "remoteCluster": undefined, - "successfulReadRequestCount": undefined, - "successfulWriteRequestsCount": undefined, - "timeSinceLastReadMs": undefined, - "totalReadRemoteExecTimeMs": undefined, - "totalReadTimeMs": undefined, - "totalWriteTimeMs": undefined, - "writeBufferOperationsCount": undefined, - "writeBufferSizeBytes": undefined, - }, - ], - "status": "active", -} -`; - -exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = ` -Object { - "bytesReadCount": "bytes read", - "failedReadRequestsCount": "failed read requests", - "failedWriteRequestsCount": "failed write requests", - "followerGlobalCheckpoint": "follower global checkpoint", - "followerMappingVersion": "follower mapping version", - "followerMaxSequenceNum": "follower max seq no", - "followerSettingsVersion": "follower settings version", - "id": "shard id", - "lastRequestedSequenceNum": "last requested seq no", - "leaderGlobalCheckpoint": "leader global checkpoint", - "leaderIndex": "leader index", - "leaderMaxSequenceNum": "leader max seq no", - "operationsReadCount": "operations read", - "operationsWrittenCount": "operations written", - "outstandingReadRequestsCount": "outstanding read requests", - "outstandingWriteRequestsCount": "outstanding write requests", - "readExceptions": Array [ - "read exception", - ], - "remoteCluster": "remote cluster", - "successfulReadRequestCount": "successful read requests", - "successfulWriteRequestsCount": "successful write requests", - "timeSinceLastReadMs": "time since last read millis", - "totalReadRemoteExecTimeMs": "total read remote exec time millis", - "totalReadTimeMs": "total read time millis", - "totalWriteTimeMs": "total write time millis", - "writeBufferOperationsCount": "write buffer operation count", - "writeBufferSizeBytes": "write buffer size in bytes", -} -`; - -exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = ` -Object { - "leader_index": "leader index", - "max_outstanding_read_requests": "foo", - "max_outstanding_write_requests": "foo", - "max_read_request_operation_count": "foo", - "max_read_request_size": "foo", - "max_retry_delay": "foo", - "max_write_buffer_count": "foo", - "max_write_buffer_size": "foo", - "max_write_request_operation_count": "foo", - "max_write_request_size": "foo", - "read_poll_timeout": "foo", - "remote_cluster": "remote cluster", -} -`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js deleted file mode 100644 index ae13c625a7d80..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js +++ /dev/null @@ -1,41 +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. - */ - -export const deserializeAutoFollowPattern = ( - { - name, - pattern: { - active, - // eslint-disable-next-line camelcase - remote_cluster, - // eslint-disable-next-line camelcase - leader_index_patterns, - // eslint-disable-next-line camelcase - follow_index_pattern, - }, - } = { - pattern: {}, - } -) => ({ - name, - active, - remoteCluster: remote_cluster, - leaderIndexPatterns: leader_index_patterns, - followIndexPattern: follow_index_pattern, -}); - -export const deserializeListAutoFollowPatterns = autoFollowPatterns => - autoFollowPatterns.map(deserializeAutoFollowPattern); - -export const serializeAutoFollowPattern = ({ - remoteCluster, - leaderIndexPatterns, - followIndexPattern, -}) => ({ - remote_cluster: remoteCluster, - leader_index_patterns: leaderIndexPatterns, - follow_index_pattern: followIndexPattern, -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js deleted file mode 100644 index eef87a6cc4c89..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js +++ /dev/null @@ -1,102 +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 { - deserializeAutoFollowPattern, - deserializeListAutoFollowPatterns, - serializeAutoFollowPattern, -} from './auto_follow_pattern_serialization'; - -describe('[CCR] auto-follow_serialization', () => { - describe('deserializeAutoFollowPattern()', () => { - it('should return empty object if name or esObject are not provided', () => { - expect(deserializeAutoFollowPattern()).toEqual({}); - }); - - it('should deserialize Elasticsearch object', () => { - const expected = { - name: 'some-name', - remoteCluster: 'foo', - leaderIndexPatterns: ['foo-*'], - followIndexPattern: 'bar', - }; - - const esObject = { - name: 'some-name', - pattern: { - remote_cluster: expected.remoteCluster, - leader_index_patterns: expected.leaderIndexPatterns, - follow_index_pattern: expected.followIndexPattern, - }, - }; - - expect(deserializeAutoFollowPattern(esObject)).toEqual(expected); - }); - }); - - describe('deserializeListAutoFollowPatterns()', () => { - it('should deserialize list of Elasticsearch objects', () => { - const name1 = 'foo1'; - const name2 = 'foo2'; - - const expected = [ - { - name: name1, - remoteCluster: 'foo1', - leaderIndexPatterns: ['foo1-*'], - followIndexPattern: 'bar2', - }, - { - name: name2, - remoteCluster: 'foo2', - leaderIndexPatterns: ['foo2-*'], - followIndexPattern: 'bar2', - }, - ]; - - const esObjects = { - patterns: [ - { - name: name1, - pattern: { - remote_cluster: expected[0].remoteCluster, - leader_index_patterns: expected[0].leaderIndexPatterns, - follow_index_pattern: expected[0].followIndexPattern, - }, - }, - { - name: name2, - pattern: { - remote_cluster: expected[1].remoteCluster, - leader_index_patterns: expected[1].leaderIndexPatterns, - follow_index_pattern: expected[1].followIndexPattern, - }, - }, - ], - }; - - expect(deserializeListAutoFollowPatterns(esObjects.patterns)).toEqual(expected); - }); - }); - - describe('serializeAutoFollowPattern()', () => { - it('should serialize object to Elasticsearch object', () => { - const expected = { - remote_cluster: 'foo', - leader_index_patterns: ['bar-*'], - follow_index_pattern: 'faz', - }; - - const object = { - remoteCluster: expected.remote_cluster, - leaderIndexPatterns: expected.leader_index_patterns, - followIndexPattern: expected.follow_index_pattern, - }; - - expect(serializeAutoFollowPattern(object)).toEqual(expected); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js deleted file mode 100644 index c41fde8f7818d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js +++ /dev/null @@ -1,135 +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. - */ - -/* eslint-disable camelcase */ -export const deserializeShard = ({ - remote_cluster, - leader_index, - shard_id, - leader_global_checkpoint, - leader_max_seq_no, - follower_global_checkpoint, - follower_max_seq_no, - last_requested_seq_no, - outstanding_read_requests, - outstanding_write_requests, - write_buffer_operation_count, - write_buffer_size_in_bytes, - follower_mapping_version, - follower_settings_version, - total_read_time_millis, - total_read_remote_exec_time_millis, - successful_read_requests, - failed_read_requests, - operations_read, - bytes_read, - total_write_time_millis, - successful_write_requests, - failed_write_requests, - operations_written, - read_exceptions, - time_since_last_read_millis, -}) => ({ - id: shard_id, - remoteCluster: remote_cluster, - leaderIndex: leader_index, - leaderGlobalCheckpoint: leader_global_checkpoint, - leaderMaxSequenceNum: leader_max_seq_no, - followerGlobalCheckpoint: follower_global_checkpoint, - followerMaxSequenceNum: follower_max_seq_no, - lastRequestedSequenceNum: last_requested_seq_no, - outstandingReadRequestsCount: outstanding_read_requests, - outstandingWriteRequestsCount: outstanding_write_requests, - writeBufferOperationsCount: write_buffer_operation_count, - writeBufferSizeBytes: write_buffer_size_in_bytes, - followerMappingVersion: follower_mapping_version, - followerSettingsVersion: follower_settings_version, - totalReadTimeMs: total_read_time_millis, - totalReadRemoteExecTimeMs: total_read_remote_exec_time_millis, - successfulReadRequestCount: successful_read_requests, - failedReadRequestsCount: failed_read_requests, - operationsReadCount: operations_read, - bytesReadCount: bytes_read, - totalWriteTimeMs: total_write_time_millis, - successfulWriteRequestsCount: successful_write_requests, - failedWriteRequestsCount: failed_write_requests, - operationsWrittenCount: operations_written, - // This is an array of exception objects - readExceptions: read_exceptions, - timeSinceLastReadMs: time_since_last_read_millis, -}); -/* eslint-enable camelcase */ - -/* eslint-disable camelcase */ -export const deserializeFollowerIndex = ({ - follower_index, - remote_cluster, - leader_index, - status, - parameters: { - max_read_request_operation_count, - max_outstanding_read_requests, - max_read_request_size, - max_write_request_operation_count, - max_write_request_size, - max_outstanding_write_requests, - max_write_buffer_count, - max_write_buffer_size, - max_retry_delay, - read_poll_timeout, - } = {}, - shards, -}) => ({ - name: follower_index, - remoteCluster: remote_cluster, - leaderIndex: leader_index, - status, - maxReadRequestOperationCount: max_read_request_operation_count, - maxOutstandingReadRequests: max_outstanding_read_requests, - maxReadRequestSize: max_read_request_size, - maxWriteRequestOperationCount: max_write_request_operation_count, - maxWriteRequestSize: max_write_request_size, - maxOutstandingWriteRequests: max_outstanding_write_requests, - maxWriteBufferCount: max_write_buffer_count, - maxWriteBufferSize: max_write_buffer_size, - maxRetryDelay: max_retry_delay, - readPollTimeout: read_poll_timeout, - shards: shards && shards.map(deserializeShard), -}); -/* eslint-enable camelcase */ - -export const deserializeListFollowerIndices = followerIndices => - followerIndices.map(deserializeFollowerIndex); - -export const serializeAdvancedSettings = ({ - maxReadRequestOperationCount, - maxOutstandingReadRequests, - maxReadRequestSize, - maxWriteRequestOperationCount, - maxWriteRequestSize, - maxOutstandingWriteRequests, - maxWriteBufferCount, - maxWriteBufferSize, - maxRetryDelay, - readPollTimeout, -}) => ({ - max_read_request_operation_count: maxReadRequestOperationCount, - max_outstanding_read_requests: maxOutstandingReadRequests, - max_read_request_size: maxReadRequestSize, - max_write_request_operation_count: maxWriteRequestOperationCount, - max_write_request_size: maxWriteRequestSize, - max_outstanding_write_requests: maxOutstandingWriteRequests, - max_write_buffer_count: maxWriteBufferCount, - max_write_buffer_size: maxWriteBufferSize, - max_retry_delay: maxRetryDelay, - read_poll_timeout: readPollTimeout, -}); - -export const serializeFollowerIndex = followerIndex => ({ - remote_cluster: followerIndex.remoteCluster, - leader_index: followerIndex.leaderIndex, - ...serializeAdvancedSettings(followerIndex), -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js deleted file mode 100644 index e1df917d899ad..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js +++ /dev/null @@ -1,175 +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 { - deserializeShard, - deserializeFollowerIndex, - deserializeListFollowerIndices, - serializeFollowerIndex, -} from './follower_index_serialization'; - -describe('[CCR] follower index serialization', () => { - describe('deserializeShard()', () => { - it('deserializes shard', () => { - const serializedShard = { - remote_cluster: 'remote cluster', - leader_index: 'leader index', - shard_id: 'shard id', - leader_global_checkpoint: 'leader global checkpoint', - leader_max_seq_no: 'leader max seq no', - follower_global_checkpoint: 'follower global checkpoint', - follower_max_seq_no: 'follower max seq no', - last_requested_seq_no: 'last requested seq no', - outstanding_read_requests: 'outstanding read requests', - outstanding_write_requests: 'outstanding write requests', - write_buffer_operation_count: 'write buffer operation count', - write_buffer_size_in_bytes: 'write buffer size in bytes', - follower_mapping_version: 'follower mapping version', - follower_settings_version: 'follower settings version', - total_read_time_millis: 'total read time millis', - total_read_remote_exec_time_millis: 'total read remote exec time millis', - successful_read_requests: 'successful read requests', - failed_read_requests: 'failed read requests', - operations_read: 'operations read', - bytes_read: 'bytes read', - total_write_time_millis: 'total write time millis', - successful_write_requests: 'successful write requests', - failed_write_requests: 'failed write requests', - operations_written: 'operations written', - read_exceptions: ['read exception'], - time_since_last_read_millis: 'time since last read millis', - }; - - expect(deserializeShard(serializedShard)).toMatchSnapshot(); - }); - }); - - describe('deserializeFollowerIndex()', () => { - it('deserializes Elasticsearch follower index object', () => { - const serializedFollowerIndex = { - index: 'follower index name', - status: 'active', - shards: [ - { - shard_id: 'shard 1', - }, - { - shard_id: 'shard 2', - }, - ], - }; - - expect(deserializeFollowerIndex(serializedFollowerIndex)).toMatchSnapshot(); - }); - }); - - describe('deserializeListFollowerIndices()', () => { - it('deserializes list of Elasticsearch follower index objects', () => { - const serializedFollowerIndexList = [ - { - follower_index: 'follower index 1', - remote_cluster: 'cluster 1', - leader_index: 'leader 1', - status: 'active', - parameters: { - max_read_request_operation_count: 1, - max_outstanding_read_requests: 1, - max_read_request_size: 1, - max_write_request_operation_count: 1, - max_write_request_size: 1, - max_outstanding_write_requests: 1, - max_write_buffer_count: 1, - max_write_buffer_size: 1, - max_retry_delay: 1, - read_poll_timeout: 1, - }, - shards: [], - }, - { - follower_index: 'follower index 2', - remote_cluster: 'cluster 2', - leader_index: 'leader 2', - status: 'paused', - parameters: { - max_read_request_operation_count: 2, - max_outstanding_read_requests: 2, - max_read_request_size: 2, - max_write_request_operation_count: 2, - max_write_request_size: 2, - max_outstanding_write_requests: 2, - max_write_buffer_count: 2, - max_write_buffer_size: 2, - max_retry_delay: 2, - read_poll_timeout: 2, - }, - shards: [], - }, - ]; - - const deserializedFollowerIndexList = [ - { - name: 'follower index 1', - remoteCluster: 'cluster 1', - leaderIndex: 'leader 1', - status: 'active', - maxReadRequestOperationCount: 1, - maxOutstandingReadRequests: 1, - maxReadRequestSize: 1, - maxWriteRequestOperationCount: 1, - maxWriteRequestSize: 1, - maxOutstandingWriteRequests: 1, - maxWriteBufferCount: 1, - maxWriteBufferSize: 1, - maxRetryDelay: 1, - readPollTimeout: 1, - shards: [], - }, - { - name: 'follower index 2', - remoteCluster: 'cluster 2', - leaderIndex: 'leader 2', - status: 'paused', - maxReadRequestOperationCount: 2, - maxOutstandingReadRequests: 2, - maxReadRequestSize: 2, - maxWriteRequestOperationCount: 2, - maxWriteRequestSize: 2, - maxOutstandingWriteRequests: 2, - maxWriteBufferCount: 2, - maxWriteBufferSize: 2, - maxRetryDelay: 2, - readPollTimeout: 2, - shards: [], - }, - ]; - - expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual( - deserializedFollowerIndexList - ); - }); - }); - - describe('serializeFollowerIndex()', () => { - it('serializes object to Elasticsearch follower index object', () => { - const deserializedFollowerIndex = { - remoteCluster: 'remote cluster', - leaderIndex: 'leader index', - maxReadRequestOperationCount: 'foo', - maxOutstandingReadRequests: 'foo', - maxReadRequestSize: 'foo', - maxWriteRequestOperationCount: 'foo', - maxWriteRequestSize: 'foo', - maxOutstandingWriteRequests: 'foo', - maxWriteBufferCount: 'foo', - maxWriteBufferSize: 'foo', - maxRetryDelay: 'foo', - readPollTimeout: 'foo', - }; - - expect(serializeFollowerIndex(deserializedFollowerIndex)).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js deleted file mode 100644 index 3d8c97f45327c..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js +++ /dev/null @@ -1,28 +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. - */ -export const arrify = val => (Array.isArray(val) ? val : [val]); - -/** - * Utilty to add some latency in a Promise chain - * - * @param {number} time Time in millisecond to wait - */ -export const wait = (time = 1000) => data => { - return new Promise(resolve => { - setTimeout(() => resolve(data), time); - }); -}; - -/** - * Utility to remove empty fields ("") from a request body - */ -export const removeEmptyFields = body => - Object.entries(body).reduce((acc, [key, value]) => { - if (value !== '') { - acc[key] = value; - } - return acc; - }, {}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js deleted file mode 100644 index 804fe80cd27b4..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js +++ /dev/null @@ -1,49 +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 { getRandomString } from '../../../../test_utils'; - -export const getAutoFollowPatternMock = ( - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndexPatterns = [getRandomString()], - followIndexPattern = getRandomString() -) => ({ - name, - pattern: { - remote_cluster: remoteCluster, - leader_index_patterns: leaderIndexPatterns, - follow_index_pattern: followIndexPattern, - }, -}); - -export const getAutoFollowPatternListMock = (total = 3) => { - const list = { - patterns: [], - }; - - let i = total; - while (i--) { - list.patterns.push(getAutoFollowPatternMock()); - } - - return list; -}; - -// ----------------- -// Client test mock -// ----------------- -export const getAutoFollowPatternClientMock = ({ - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndexPatterns = [`${getRandomString()}-*`], - followIndexPattern = getRandomString(), -}) => ({ - name, - remoteCluster, - leaderIndexPatterns, - followIndexPattern, -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js deleted file mode 100644 index a042375e82715..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js +++ /dev/null @@ -1,45 +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. - */ - -/** - * Errors mocks to throw during development to help visualizing - * the different flows in the UI - * - * TODO: Consult the ES team and make sure the error shapes are correct - * for each statusCode. - */ - -const error400 = new Error('Something went wrong'); -error400.statusCode = 400; -error400.response = ` - { - "error": { - "root_cause": [ - { - "type": "x_content_parse_exception", - "reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found" - } - ], - "type": "x_content_parse_exception", - "reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found" - }, - "status": 400 -}`; - -const error403 = new Error('Unauthorized'); -error403.statusCode = 403; -error403.response = ` - { - "acknowledged": true, - "trial_was_started": false, - "error_message": "Operation failed: Trial was already activated." - } -`; - -export const esErrors = { - 400: error400, - 403: error403, -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js deleted file mode 100644 index 6c535a665978c..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js +++ /dev/null @@ -1,216 +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. - */ - -const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies -const chance = new Chance(); -import { getRandomString } from '../../../../test_utils'; - -const serializeShard = ({ - id, - remoteCluster, - leaderIndex, - leaderGlobalCheckpoint, - leaderMaxSequenceNum, - followerGlobalCheckpoint, - followerMaxSequenceNum, - lastRequestedSequenceNum, - outstandingReadRequestsCount, - outstandingWriteRequestsCount, - writeBufferOperationsCount, - writeBufferSizeBytes, - followerMappingVersion, - followerSettingsVersion, - totalReadTimeMs, - totalReadRemoteExecTimeMs, - successfulReadRequestCount, - failedReadRequestsCount, - operationsReadCount, - bytesReadCount, - totalWriteTimeMs, - successfulWriteRequestsCount, - failedWriteRequestsCount, - operationsWrittenCount, - readExceptions, - timeSinceLastReadMs, -}) => ({ - shard_id: id, - remote_cluster: remoteCluster, - leader_index: leaderIndex, - leader_global_checkpoint: leaderGlobalCheckpoint, - leader_max_seq_no: leaderMaxSequenceNum, - follower_global_checkpoint: followerGlobalCheckpoint, - follower_max_seq_no: followerMaxSequenceNum, - last_requested_seq_no: lastRequestedSequenceNum, - outstanding_read_requests: outstandingReadRequestsCount, - outstanding_write_requests: outstandingWriteRequestsCount, - write_buffer_operation_count: writeBufferOperationsCount, - write_buffer_size_in_bytes: writeBufferSizeBytes, - follower_mapping_version: followerMappingVersion, - follower_settings_version: followerSettingsVersion, - total_read_time_millis: totalReadTimeMs, - total_read_remote_exec_time_millis: totalReadRemoteExecTimeMs, - successful_read_requests: successfulReadRequestCount, - failed_read_requests: failedReadRequestsCount, - operations_read: operationsReadCount, - bytes_read: bytesReadCount, - total_write_time_millis: totalWriteTimeMs, - successful_write_requests: successfulWriteRequestsCount, - failed_write_requests: failedWriteRequestsCount, - operations_written: operationsWrittenCount, - read_exceptions: readExceptions, - time_since_last_read_millis: timeSinceLastReadMs, -}); - -export const getFollowerIndexStatsMock = ( - name = chance.string(), - shards = [ - { - id: chance.string(), - remoteCluster: chance.string(), - leaderIndex: chance.string(), - leaderGlobalCheckpoint: chance.integer(), - leaderMaxSequenceNum: chance.integer(), - followerGlobalCheckpoint: chance.integer(), - followerMaxSequenceNum: chance.integer(), - lastRequestedSequenceNum: chance.integer(), - outstandingReadRequestsCount: chance.integer(), - outstandingWriteRequestsCount: chance.integer(), - writeBufferOperationsCount: chance.integer(), - writeBufferSizeBytes: chance.integer(), - followerMappingVersion: chance.integer(), - followerSettingsVersion: chance.integer(), - totalReadTimeMs: chance.integer(), - totalReadRemoteExecTimeMs: chance.integer(), - successfulReadRequestCount: chance.integer(), - failedReadRequestsCount: chance.integer(), - operationsReadCount: chance.integer(), - bytesReadCount: chance.integer(), - totalWriteTimeMs: chance.integer(), - successfulWriteRequestsCount: chance.integer(), - failedWriteRequestsCount: chance.integer(), - operationsWrittenCount: chance.integer(), - readExceptions: [chance.string()], - timeSinceLastReadMs: chance.integer(), - }, - ] -) => ({ - index: name, - shards: shards.map(serializeShard), -}); - -export const getFollowerIndexListStatsMock = (total = 3, names) => { - const list = { - follow_stats: { - indices: [], - }, - }; - - for (let i = 0; i < total; i++) { - list.follow_stats.indices.push(getFollowerIndexStatsMock(names[i])); - } - - return list; -}; - -export const getFollowerIndexInfoMock = ( - name = chance.string(), - status = chance.string(), - parameters = { - maxReadRequestOperationCount: chance.string(), - maxOutstandingReadRequests: chance.string(), - maxReadRequestSize: chance.string(), - maxWriteRequestOperationCount: chance.string(), - maxWriteRequestSize: chance.string(), - maxOutstandingWriteRequests: chance.string(), - maxWriteBufferCount: chance.string(), - maxWriteBufferSize: chance.string(), - maxRetryDelay: chance.string(), - readPollTimeout: chance.string(), - } -) => { - return { - follower_index: name, - status, - max_read_request_operation_count: parameters.maxReadRequestOperationCount, - max_outstanding_read_requests: parameters.maxOutstandingReadRequests, - max_read_request_size: parameters.maxReadRequestSize, - max_write_request_operation_count: parameters.maxWriteRequestOperationCount, - max_write_request_size: parameters.maxWriteRequestSize, - max_outstanding_write_requests: parameters.maxOutstandingWriteRequests, - max_write_buffer_count: parameters.maxWriteBufferCount, - max_write_buffer_size: parameters.maxWriteBufferSize, - max_retry_delay: parameters.maxRetryDelay, - read_poll_timeout: parameters.readPollTimeout, - }; -}; - -export const getFollowerIndexListInfoMock = (total = 3) => { - const list = { - follower_indices: [], - }; - - for (let i = 0; i < total; i++) { - list.follower_indices.push(getFollowerIndexInfoMock()); - } - - return list; -}; - -// ----------------- -// Client test mock -// ----------------- - -export const getFollowerIndexMock = ({ - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndex = getRandomString(), - status = 'Active', -} = {}) => ({ - name, - remoteCluster, - leaderIndex, - status, - maxReadRequestOperationCount: chance.integer(), - maxOutstandingReadRequests: chance.integer(), - maxReadRequestSize: getRandomString({ length: 5 }), - maxWriteRequestOperationCount: chance.integer(), - maxWriteRequestSize: '9223372036854775807b', - maxOutstandingWriteRequests: chance.integer(), - maxWriteBufferCount: chance.integer(), - maxWriteBufferSize: getRandomString({ length: 5 }), - maxRetryDelay: getRandomString({ length: 5 }), - readPollTimeout: getRandomString({ length: 5 }), - shards: [ - { - id: 0, - remoteCluster: remoteCluster, - leaderIndex: leaderIndex, - leaderGlobalCheckpoint: chance.integer(), - leaderMaxSequenceNum: chance.integer(), - followerGlobalCheckpoint: chance.integer(), - followerMaxSequenceNum: chance.integer(), - lastRequestedSequenceNum: chance.integer(), - outstandingReadRequestsCount: chance.integer(), - outstandingWriteRequestsCount: chance.integer(), - writeBufferOperationsCount: chance.integer(), - writeBufferSizeBytes: chance.integer(), - followerMappingVersion: chance.integer(), - followerSettingsVersion: chance.integer(), - totalReadTimeMs: chance.integer(), - totalReadRemoteExecTimeMs: chance.integer(), - successfulReadRequestCount: chance.integer(), - failedReadRequestsCount: chance.integer(), - operationsReadCount: chance.integer(), - bytesReadCount: chance.integer(), - totalWriteTimeMs: chance.integer(), - successfulWriteRequestsCount: chance.integer(), - failedWriteRequestsCount: chance.integer(), - operationsWrittenCount: chance.integer(), - readExceptions: [], - timeSinceLastReadMs: chance.integer(), - }, - ], -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js deleted file mode 100644 index ccfdf8b19f3ee..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js +++ /dev/null @@ -1,16 +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. - */ - -export { getAutoFollowPatternMock, getAutoFollowPatternListMock } from './auto_follow_pattern'; - -export { esErrors } from './es_errors'; - -export { - getFollowerIndexStatsMock, - getFollowerIndexListStatsMock, - getFollowerIndexInfoMock, - getFollowerIndexListInfoMock, -} from './follower_index'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/index.js b/x-pack/legacy/plugins/cross_cluster_replication/index.js deleted file mode 100644 index aff4cc5b56738..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/index.js +++ /dev/null @@ -1,57 +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 { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { plugin } from './server/np_ready'; - -export function crossClusterReplication(kibana) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.ccr', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main', 'remoteClusters', 'index_management'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/cross_cluster_replication'], - injectDefaultVars(server) { - const config = server.config(); - return { - ccrUiEnabled: - config.get('xpack.ccr.ui.enabled') && config.get('xpack.remote_clusters.ui.enabled'), - }; - }, - }, - - config(Joi) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - }).default(); - }, - isEnabled(config) { - return ( - config.get('xpack.ccr.enabled') && - config.get('xpack.index_management.enabled') && - config.get('xpack.remote_clusters.enabled') - ); - }, - init: function initCcrPlugin(server) { - plugin({}).setup(server.newPlatform.setup.core, { - indexManagement: server.newPlatform.setup.plugins.indexManagement, - __LEGACY: { - server, - ccrUIEnabled: server.config().get('xpack.ccr.ui.enabled'), - }, - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/index.js deleted file mode 100644 index e92c44da34474..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js +++ /dev/null @@ -1,7 +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 './register_routes'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss deleted file mode 100644 index 31317e16e3e9f..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Cross-Cluster Replication plugin styles - -// Prefix all styles with "ccr" to avoid conflicts. -// Examples -// ccrChart -// ccrChart__legend -// ccrChart__legend--small -// ccrChart__legend-isLoading - -@import 'np_ready/app/app'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/main.html b/x-pack/legacy/plugins/cross_cluster_replication/public/main.html deleted file mode 100644 index 2129f26267827..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/main.html +++ /dev/null @@ -1,3 +0,0 @@ - -

- diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss deleted file mode 100644 index 5ee862b1d9e44..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss +++ /dev/null @@ -1,14 +0,0 @@ -.ccrFollowerIndicesFormRow { - padding-bottom: 0; -} - -.ccrFollowerIndicesHelpText { - transform: translateY(-3px); -} - -/** - * 1. Prevent context menu popover appearing above confirmation modal - */ -.ccrFollowerIndicesDetailPanel { - z-index: $euiZMask - 1; /* 1 */ -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js deleted file mode 100644 index 968646a4bd1b0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js +++ /dev/null @@ -1,220 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageContent, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { BASE_PATH } from '../../../common/constants'; -import { getFatalErrors } from './services/notifications'; -import { SectionError } from './components'; -import routing from './services/routing'; -import { loadPermissions } from './services/api'; - -import { - CrossClusterReplicationHome, - AutoFollowPatternAdd, - AutoFollowPatternEdit, - FollowerIndexAdd, - FollowerIndexEdit, -} from './sections'; - -class AppComponent extends Component { - static propTypes = { - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired, - }).isRequired, - }; - - constructor(...args) { - super(...args); - this.registerRouter(); - - this.state = { - isFetchingPermissions: false, - fetchPermissionError: undefined, - hasPermission: false, - missingClusterPrivileges: [], - }; - } - - UNSAFE_componentWillMount() { - routing.userHasLeftApp = false; - } - - componentDidMount() { - this.checkPermissions(); - } - - componentWillUnmount() { - routing.userHasLeftApp = true; - } - - async checkPermissions() { - this.setState({ - isFetchingPermissions: true, - }); - - try { - const { hasPermission, missingClusterPrivileges } = await loadPermissions(); - - this.setState({ - isFetchingPermissions: false, - hasPermission, - missingClusterPrivileges, - }); - } catch (error) { - // Expect an error in the shape provided by Angular's $http service. - if (error && error.body) { - return this.setState({ - isFetchingPermissions: false, - fetchPermissionError: error, - }); - } - - // This error isn't an HTTP error, so let the fatal error screen tell the user something - // unexpected happened. - getFatalErrors().add( - error, - i18n.translate('xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle', { - defaultMessage: 'Cross-Cluster Replication app', - }) - ); - } - } - - registerRouter() { - const { history, location } = this.props; - routing.reactRouter = { - history, - route: { - location, - }, - }; - } - - render() { - const { - isFetchingPermissions, - fetchPermissionError, - hasPermission, - missingClusterPrivileges, - } = this.state; - - if (isFetchingPermissions) { - return ( - - - - - - - - -

- -

-
-
-
-
- ); - } - - if (fetchPermissionError) { - return ( - - - } - error={fetchPermissionError} - /> - - - - ); - } - - if (!hasPermission) { - return ( - - - - - } - body={ -

- -

- } - /> -
- ); - } - - return ( -
- - - - - - - - -
- ); - } -} - -export const App = withRouter(AppComponent); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js deleted file mode 100644 index cc81fce4eebe7..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js +++ /dev/null @@ -1,25 +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 React from 'react'; -import { render } from 'react-dom'; -import { Provider } from 'react-redux'; -import { HashRouter } from 'react-router-dom'; - -import { App } from './app'; -import { ccrStore } from './store'; - -export const renderReact = async (elem, I18nContext) => { - render( - - - - - - - , - elem - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js deleted file mode 100644 index 1a6d5e6efe35a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js +++ /dev/null @@ -1,370 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; - -import { - EuiButtonEmpty, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiHealth, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, - EuiTextColor, - EuiTitle, -} from '@elastic/eui'; - -import { - AutoFollowPatternIndicesPreview, - AutoFollowPatternActionMenu, -} from '../../../../../components'; - -export class DetailPanel extends Component { - static propTypes = { - apiStatus: PropTypes.string, - autoFollowPatternId: PropTypes.string, - autoFollowPattern: PropTypes.object, - closeDetailPanel: PropTypes.func.isRequired, - }; - - renderAutoFollowPattern({ - followIndexPatternPrefix, - followIndexPatternSuffix, - remoteCluster, - leaderIndexPatterns, - active, - }) { - return ( - <> -
- - - - - - - - - {!active ? ( - - - - ) : ( - - - - )} - - -
- - -
- -

- -

-
- - - - - - - - - - - - - - {remoteCluster} - - - - - - - - - - - - {leaderIndexPatterns.join(', ')} - - - - - - - - - - - - - - - - {followIndexPatternPrefix || ( - - - - )} - - - - - - - - - - - - {followIndexPatternSuffix || ( - - - - )} - - - - -
- - ); - } - - renderIndicesPreview(prefix, suffix, leaderIndexPatterns) { - return ( -
- -
- ); - } - - renderAutoFollowPatternNotFound() { - return ( - - - - - - - - - - - - - - - - ); - } - - renderAutoFollowPatternErrors(autoFollowPattern) { - if (!autoFollowPattern.errors.length) { - return null; - } - - return ( -
- - - - - - - -

- -

-
-
-
- - -
    - {autoFollowPattern.errors.map((error, i) => ( -
  • - {error.autoFollowException.reason} -
  • - ))} -
-
-
- ); - } - - renderFlyoutBody() { - const { autoFollowPattern } = this.props; - - if (!autoFollowPattern) { - return this.renderAutoFollowPatternNotFound(); - } - - const { - followIndexPatternPrefix, - followIndexPatternSuffix, - leaderIndexPatterns, - } = autoFollowPattern; - - let indexManagementFilter; - - if (followIndexPatternPrefix) { - indexManagementFilter = `name:${followIndexPatternPrefix}`; - } else if (followIndexPatternSuffix) { - indexManagementFilter = `name:${followIndexPatternSuffix}`; - } - - const indexManagementUri = getIndexListUri(indexManagementFilter); - - return ( - - {this.renderAutoFollowPattern(autoFollowPattern)} - - - - {this.renderIndicesPreview( - followIndexPatternPrefix, - followIndexPatternSuffix, - leaderIndexPatterns - )} - - - - - - - - - - {this.renderAutoFollowPatternErrors(autoFollowPattern)} - - ); - } - - renderFlyoutFooter() { - const { autoFollowPattern, closeDetailPanel } = this.props; - - return ( - - - - - - - - - {autoFollowPattern && ( - - - - - - - - )} - - - ); - } - - render() { - const { autoFollowPatternId, closeDetailPanel } = this.props; - - return ( - - - -

{autoFollowPatternId}

-
-
- - {this.renderFlyoutBody()} - {this.renderFlyoutFooter()} -
- ); - } -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js deleted file mode 100644 index 3e8cf6d3e2f78..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js +++ /dev/null @@ -1,510 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiCodeEditor, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiHealth, - EuiIcon, - EuiLoadingSpinner, - EuiSpacer, - EuiText, - EuiTextColor, - EuiTitle, -} from '@elastic/eui'; -import 'brace/theme/textmate'; - -import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; -import { API_STATUS } from '../../../../../constants'; -import { ContextMenu } from '../context_menu'; - -export class DetailPanel extends Component { - static propTypes = { - apiStatus: PropTypes.string, - followerIndexId: PropTypes.string, - followerIndex: PropTypes.object, - closeDetailPanel: PropTypes.func.isRequired, - }; - - renderFollowerIndex() { - const { - followerIndex: { - remoteCluster, - leaderIndex, - isPaused, - shards, - maxReadRequestOperationCount, - maxOutstandingReadRequests, - maxReadRequestSize, - maxWriteRequestOperationCount, - maxWriteRequestSize, - maxOutstandingWriteRequests, - maxWriteBufferCount, - maxWriteBufferSize, - maxRetryDelay, - readPollTimeout, - }, - } = this.props; - - return ( - - -
- - - - - - - - - - - {isPaused ? ( - - - - ) : ( - - - - )} - - - - - - - - - - - - - - - - {remoteCluster} - - - - - - - - - - - - {leaderIndex} - - - - -
- - - -
- -

- -

-
- - - - {isPaused ? ( - - } - /> - ) : ( - - - - - - - - - - - {maxReadRequestOperationCount} - - - - - - - - - - - - {maxOutstandingReadRequests} - - - - - - - - - - - - - - - - {maxReadRequestSize} - - - - - - - - - - - - {maxWriteRequestOperationCount} - - - - - - - - - - - - - - - - {maxWriteRequestSize} - - - - - - - - - - - - {maxOutstandingWriteRequests} - - - - - - - - - - - - - - - - {maxWriteBufferCount} - - - - - - - - - - - - {maxWriteBufferSize} - - - - - - - - - - - - - - - - {maxRetryDelay} - - - - - - - - - - - - {readPollTimeout} - - - - - )} -
- - - -
- {shards && - shards.map((shard, i) => ( - - - -

- -

-
- - -
- ))} -
-
-
- ); - } - - renderContent() { - const { apiStatus, followerIndex } = this.props; - - if (apiStatus === API_STATUS.LOADING) { - return ( - - - - - - - - - - - - - - - - ); - } - - if (!followerIndex) { - return ( - - - - - - - - - - - - - - - - ); - } - - return this.renderFollowerIndex(); - } - - renderFooter() { - const { followerIndexId, followerIndex, closeDetailPanel } = this.props; - - // Use ID instead of followerIndex, because followerIndex may not be loaded yet. - const indexManagementUri = getIndexListUri(`name:${followerIndexId}`); - - return ( - - - - - - - - - - - - - - - - - {followerIndex && ( - - - } - followerIndices={[followerIndex]} - testSubj="manageButton" - /> - - )} - - - - - ); - } - - render() { - const { followerIndexId, closeDetailPanel } = this.props; - - return ( - - - -

{followerIndexId}

-
-
- - {this.renderContent()} - {this.renderFooter()} -
- ); - } -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts deleted file mode 100644 index b7c75108d4ef0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts +++ /dev/null @@ -1,10 +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. - */ - -jest.mock('./breadcrumbs', () => ({ - ...jest.requireActual('./breadcrumbs'), - setBreadcrumbs: jest.fn(), -})); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts deleted file mode 100644 index dc64cdee07f7d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts +++ /dev/null @@ -1,47 +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 { i18n } from '@kbn/i18n'; -import { ChromeBreadcrumb } from 'src/core/public'; - -import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public'; - -import { BASE_PATH } from '../../../../common/constants'; - -let setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; - -export const setBreadcrumbSetter = ({ - __LEGACY, -}: { - __LEGACY: { - chrome: any; - MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; - }; -}): void => { - setBreadcrumbs = (crumbs: ChromeBreadcrumb[]) => { - __LEGACY.chrome.breadcrumbs.set([__LEGACY.MANAGEMENT_BREADCRUMB, ...crumbs]); - }; -}; - -export const listBreadcrumb = { - text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', { - defaultMessage: 'Cross-Cluster Replication', - }), - href: `#${BASE_PATH}`, -}; - -export const addBreadcrumb = { - text: i18n.translate('xpack.crossClusterReplication.addBreadcrumbTitle', { - defaultMessage: 'Add', - }), -}; - -export const editBreadcrumb = { - text: i18n.translate('xpack.crossClusterReplication.editBreadcrumbTitle', { - defaultMessage: 'Edit', - }), -}; - -export { setBreadcrumbs }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts deleted file mode 100644 index f17926d2bee10..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts +++ /dev/null @@ -1,22 +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. - */ - -let esBase: string; - -export const setDocLinks = ({ - DOC_LINK_VERSION, - ELASTIC_WEBSITE_URL, -}: { - ELASTIC_WEBSITE_URL: string; - DOC_LINK_VERSION: string; -}) => { - esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; -}; - -export const getAutoFollowPatternUrl = () => `${esBase}/ccr-put-auto-follow-pattern.html`; -export const getFollowerIndexUrl = () => `${esBase}/ccr-put-follow.html`; -export const getByteUnitsUrl = () => `${esBase}/common-options.html#byte-units`; -export const getTimeUnitsUrl = () => `${esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts deleted file mode 100644 index 5e1c3e9e99437..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts +++ /dev/null @@ -1,20 +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 { NotificationsSetup, IToasts, FatalErrorsSetup } from 'src/core/public'; - -let _notifications: IToasts; -let _fatalErrors: FatalErrorsSetup; - -export const setNotifications = ( - notifications: NotificationsSetup, - fatalErrorsSetup: FatalErrorsSetup -) => { - _notifications = notifications.toasts; - _fatalErrors = fatalErrorsSetup; -}; - -export const getNotifications = () => _notifications; -export const getFatalErrors = () => _fatalErrors; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js deleted file mode 100644 index 36b9c185b487d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js +++ /dev/null @@ -1,27 +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 { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; -import { UIM_APP_NAME } from '../constants'; - -export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); -export { METRIC_TYPE }; -/** - * Transparently return provided request Promise, while allowing us to track - * a successful completion of the request. - */ -export function trackUserRequest(request, actionType) { - // Only track successful actions. - return request.then(response => { - trackUiMetric(METRIC_TYPE.LOADED, actionType); - // We return the response immediately without waiting for the tracking request to resolve, - // to avoid adding additional latency. - return response; - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js deleted file mode 100644 index b81cd30f3977a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js +++ /dev/null @@ -1,257 +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 { i18n } from '@kbn/i18n'; -import { getNotifications } from '../../services/notifications'; -import { SECTIONS, API_STATUS } from '../../constants'; -import { - loadAutoFollowPatterns as loadAutoFollowPatternsRequest, - getAutoFollowPattern as getAutoFollowPatternRequest, - createAutoFollowPattern as createAutoFollowPatternRequest, - updateAutoFollowPattern as updateAutoFollowPatternRequest, - deleteAutoFollowPattern as deleteAutoFollowPatternRequest, - pauseAutoFollowPattern as pauseAutoFollowPatternRequest, - resumeAutoFollowPattern as resumeAutoFollowPatternRequest, -} from '../../services/api'; -import routing from '../../services/routing'; -import * as t from '../action_types'; -import { sendApiRequest } from './api'; -import { getSelectedAutoFollowPatternId } from '../selectors'; - -const { AUTO_FOLLOW_PATTERN: scope } = SECTIONS; - -export const selectDetailAutoFollowPattern = id => ({ - type: t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL, - payload: id, -}); - -export const selectEditAutoFollowPattern = id => ({ - type: t.AUTO_FOLLOW_PATTERN_SELECT_EDIT, - payload: id, -}); - -export const loadAutoFollowPatterns = (isUpdating = false) => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_LOAD, - scope, - status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, - handler: async () => await loadAutoFollowPatternsRequest(), - }); - -export const getAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_GET, - scope: `${scope}-get`, - handler: async () => await getAutoFollowPatternRequest(id), - }); - -export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) => - sendApiRequest({ - label: isUpdating ? t.AUTO_FOLLOW_PATTERN_UPDATE : t.AUTO_FOLLOW_PATTERN_CREATE, - status: API_STATUS.SAVING, - scope: `${scope}-save`, - handler: async () => { - if (isUpdating) { - return await updateAutoFollowPatternRequest(id, autoFollowPattern); - } - return await createAutoFollowPatternRequest({ id, ...autoFollowPattern }); - }, - onSuccess() { - const successMessage = isUpdating - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.updateAction.successNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' updated successfully`, - values: { name: id }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.addAction.successNotificationTitle', - { - defaultMessage: `Added auto-follow pattern '{name}'`, - values: { name: id }, - } - ); - - getNotifications().addSuccess(successMessage); - routing.navigate(`/auto_follow_patterns`, undefined, { - pattern: encodeURIComponent(id), - }); - }, - }); - -export const deleteAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_DELETE, - scope: `${scope}-delete`, - status: API_STATUS.DELETING, - handler: async () => deleteAutoFollowPatternRequest(id), - onSuccess(response, dispatch, getState) { - /** - * We can have 1 or more auto-follow pattern delete operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error removing {count} auto-follow patterns`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorSingleNotificationTitle', - { - defaultMessage: `Error removing the '{name}' auto-follow pattern`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsDeleted.length) { - const hasMultipleDelete = response.itemsDeleted.length > 1; - - const successMessage = hasMultipleDelete - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} auto-follow patterns were removed`, - values: { count: response.itemsDeleted.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successSingleNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' was removed`, - values: { name: response.itemsDeleted[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - - // If we've just deleted a pattern we were looking at, we need to close the panel. - const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState()); - if (response.itemsDeleted.includes(autoFollowPatternId)) { - dispatch(selectDetailAutoFollowPattern(null)); - } - } - }, - }); - -export const pauseAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_PAUSE, - scope: `${scope}-pause`, - status: API_STATUS.UPDATING, - handler: () => pauseAutoFollowPatternRequest(id), - onSuccess: response => { - /** - * We can have 1 or more auto-follow pattern pause operations - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error pausing {count} auto-follow patterns`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorSingleNotificationTitle', - { - defaultMessage: `Error pausing the '{name}' auto-follow pattern`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsPaused.length) { - const hasMultiple = response.itemsPaused.length > 1; - - const successMessage = hasMultiple - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} auto-follow patterns were paused`, - values: { count: response.itemsPaused.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successSingleNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' was paused`, - values: { name: response.itemsPaused[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - }, - }); - -export const resumeAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_RESUME, - scope: `${scope}-resume`, - status: API_STATUS.UPDATING, - handler: () => resumeAutoFollowPatternRequest(id), - onSuccess: response => { - /** - * We can have 1 or more auto-follow pattern resume operations - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error resuming {count} auto-follow patterns`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorSingleNotificationTitle', - { - defaultMessage: `Error resuming the '{name}' auto-follow pattern`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsResumed.length) { - const hasMultiple = response.itemsResumed.length > 1; - - const successMessage = hasMultiple - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} auto-follow patterns were resumed`, - values: { count: response.itemsResumed.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successSingleNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' was resumed`, - values: { name: response.itemsResumed[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - }, - }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js deleted file mode 100644 index ebdee067ced75..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js +++ /dev/null @@ -1,286 +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 { i18n } from '@kbn/i18n'; - -import routing from '../../services/routing'; -import { getNotifications } from '../../services/notifications'; -import { SECTIONS, API_STATUS } from '../../constants'; -import { - loadFollowerIndices as loadFollowerIndicesRequest, - getFollowerIndex as getFollowerIndexRequest, - createFollowerIndex as createFollowerIndexRequest, - pauseFollowerIndex as pauseFollowerIndexRequest, - resumeFollowerIndex as resumeFollowerIndexRequest, - unfollowLeaderIndex as unfollowLeaderIndexRequest, - updateFollowerIndex as updateFollowerIndexRequest, -} from '../../services/api'; -import * as t from '../action_types'; -import { sendApiRequest } from './api'; -import { getSelectedFollowerIndexId } from '../selectors'; - -const { FOLLOWER_INDEX: scope } = SECTIONS; - -export const selectDetailFollowerIndex = id => ({ - type: t.FOLLOWER_INDEX_SELECT_DETAIL, - payload: id, -}); - -export const selectEditFollowerIndex = id => ({ - type: t.FOLLOWER_INDEX_SELECT_EDIT, - payload: id, -}); - -export const loadFollowerIndices = (isUpdating = false) => - sendApiRequest({ - label: t.FOLLOWER_INDEX_LOAD, - scope, - status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, - handler: async () => await loadFollowerIndicesRequest(), - }); - -export const getFollowerIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_GET, - scope: `${scope}-get`, - handler: async () => await getFollowerIndexRequest(id), - }); - -export const saveFollowerIndex = (name, followerIndex, isUpdating = false) => - sendApiRequest({ - label: t.FOLLOWER_INDEX_CREATE, - status: API_STATUS.SAVING, - scope: `${scope}-save`, - handler: async () => { - if (isUpdating) { - return await updateFollowerIndexRequest(name, followerIndex); - } - return await createFollowerIndexRequest({ name, ...followerIndex }); - }, - onSuccess() { - const successMessage = isUpdating - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.updateAction.successNotificationTitle', - { - defaultMessage: `Follower index '{name}' updated successfully`, - values: { name }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.addAction.successNotificationTitle', - { - defaultMessage: `Added follower index '{name}'`, - values: { name }, - } - ); - - getNotifications().addSuccess(successMessage); - routing.navigate(`/follower_indices`, undefined, { - name: encodeURIComponent(name), - }); - }, - }); - -export const pauseFollowerIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_PAUSE, - status: API_STATUS.SAVING, - scope, - handler: async () => pauseFollowerIndexRequest(id), - onSuccess(response, dispatch) { - /** - * We can have 1 or more follower index pause operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error pausing {count} follower indices`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.errorSingleNotificationTitle', - { - defaultMessage: `Error pausing follower index '{name}'`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsPaused.length) { - const hasMultiplePaused = response.itemsPaused.length > 1; - - const successMessage = hasMultiplePaused - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} follower indices were paused`, - values: { count: response.itemsPaused.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.successSingleNotificationTitle', - { - defaultMessage: `Follower index '{name}' was paused`, - values: { name: response.itemsPaused[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - - // Refresh list - dispatch(loadFollowerIndices(true)); - } - }, - }); - -export const resumeFollowerIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_RESUME, - status: API_STATUS.SAVING, - scope, - handler: async () => resumeFollowerIndexRequest(id), - onSuccess(response, dispatch) { - /** - * We can have 1 or more follower index resume operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error resuming {count} follower indices`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.errorSingleNotificationTitle', - { - defaultMessage: `Error resuming follower index '{name}'`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsResumed.length) { - const hasMultipleResumed = response.itemsResumed.length > 1; - - const successMessage = hasMultipleResumed - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} follower indices were resumed`, - values: { count: response.itemsResumed.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.successSingleNotificationTitle', - { - defaultMessage: `Follower index '{name}' was resumed`, - values: { name: response.itemsResumed[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - - // Refresh list - dispatch(loadFollowerIndices(true)); - }, - }); - -export const unfollowLeaderIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_UNFOLLOW, - status: API_STATUS.DELETING, - scope: `${scope}-delete`, - handler: async () => unfollowLeaderIndexRequest(id), - onSuccess(response, dispatch, getState) { - /** - * We can have 1 or more follower index unfollow operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error unfollowing leader index of {count} follower indices`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorSingleNotificationTitle', - { - defaultMessage: `Error unfollowing leader index of follower index '{name}'`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsUnfollowed.length) { - const hasMultipleUnfollow = response.itemsUnfollowed.length > 1; - - const successMessage = hasMultipleUnfollow - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.successMultipleNotificationTitle', - { - defaultMessage: `Leader indices of {count} follower indices were unfollowed`, - values: { count: response.itemsUnfollowed.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.successSingleNotificationTitle', - { - defaultMessage: `Leader index of follower index '{name}' was unfollowed`, - values: { name: response.itemsUnfollowed[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - - if (response.itemsNotOpen.length) { - const hasMultipleNotOpen = response.itemsNotOpen.length > 1; - - const warningMessage = hasMultipleNotOpen - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningMultipleNotificationTitle', - { - defaultMessage: `{count} indices could not be re-opened`, - values: { count: response.itemsNotOpen.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningSingleNotificationTitle', - { - defaultMessage: `Index '{name}' could not be re-opened`, - values: { name: response.itemsNotOpen[0] }, - } - ); - - getNotifications().addWarning(warningMessage); - } - - // If we've just unfollowed a follower index we were looking at, we need to close the panel. - const followerIndexId = getSelectedFollowerIndexId('detail')(getState()); - if (response.itemsUnfollowed.includes(followerIndexId)) { - dispatch(selectDetailFollowerIndex(null)); - } - }, - }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts deleted file mode 100644 index 4ffe0db4e3c4e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts +++ /dev/null @@ -1,28 +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 { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; - -const propertyPath = 'isFollowerIndex'; - -const followerBadgeExtension = { - matchIndex: (index: any) => { - return get(index, propertyPath); - }, - label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', { - defaultMessage: 'Follower', - }), - color: 'default', - filterExpression: 'isFollowerIndex:true', -}; - -export const extendIndexManagement = (indexManagement?: IndexManagementPluginSetup) => { - if (indexManagement) { - indexManagement.extensionsService.addBadge(followerBadgeExtension); - } -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts deleted file mode 100644 index 11aea6b7b5de4..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts +++ /dev/null @@ -1,11 +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 { PluginInitializerContext } from 'src/core/public'; - -import { CrossClusterReplicationUIPlugin } from './plugin'; - -export const plugin = (ctx: PluginInitializerContext) => new CrossClusterReplicationUIPlugin(ctx); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts deleted file mode 100644 index 46259c698b282..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts +++ /dev/null @@ -1,44 +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 { - ChromeBreadcrumb, - CoreSetup, - Plugin, - PluginInitializerContext, - DocLinksStart, -} from 'src/core/public'; - -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; - -// @ts-ignore; -import { setHttpClient } from './app/services/api'; -import { setBreadcrumbSetter } from './app/services/breadcrumbs'; -import { setDocLinks } from './app/services/documentation_links'; -import { setNotifications } from './app/services/notifications'; -import { extendIndexManagement } from './extend_index_management'; - -interface PluginDependencies { - indexManagement: IndexManagementPluginSetup; - __LEGACY: { - chrome: any; - MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; - docLinks: DocLinksStart; - }; -} - -export class CrossClusterReplicationUIPlugin implements Plugin { - // @ts-ignore - constructor(private readonly ctx: PluginInitializerContext) {} - setup({ http, notifications, fatalErrors }: CoreSetup, deps: PluginDependencies) { - setHttpClient(http); - setBreadcrumbSetter(deps); - setDocLinks(deps.__LEGACY.docLinks); - setNotifications(notifications, fatalErrors); - extendIndexManagement(deps.indexManagement); - } - - start() {} -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js b/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js deleted file mode 100644 index 838939f46e523..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js +++ /dev/null @@ -1,94 +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 { unmountComponentAtNode } from 'react-dom'; -import chrome from 'ui/chrome'; -import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { npSetup, npStart } from 'ui/new_platform'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { i18n } from '@kbn/i18n'; - -import template from './main.html'; -import { BASE_PATH } from '../common/constants'; - -import { plugin } from './np_ready'; - -/** - * TODO: When this file is deleted, use the management section for rendering - */ -import { renderReact } from './np_ready/app'; - -const isAvailable = xpackInfo.get('features.crossClusterReplication.isAvailable'); -const isActive = xpackInfo.get('features.crossClusterReplication.isActive'); -const isLicenseOK = isAvailable && isActive; -const isCcrUiEnabled = chrome.getInjected('ccrUiEnabled'); - -if (isLicenseOK && isCcrUiEnabled) { - const esSection = management.getSection('elasticsearch'); - - esSection.register('ccr', { - visible: true, - display: i18n.translate('xpack.crossClusterReplication.appTitle', { - defaultMessage: 'Cross-Cluster Replication', - }), - order: 4, - url: `#${BASE_PATH}`, - }); - - let elem; - - const CCR_REACT_ROOT = 'ccrReactRoot'; - - plugin({}).setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - chrome, - docLinks: npStart.core.docLinks, - MANAGEMENT_BREADCRUMB, - }, - }); - - const unmountReactApp = () => elem && unmountComponentAtNode(elem); - - routes.when(`${BASE_PATH}/:section?/:subsection?/:view?/:id?`, { - template, - controllerAs: 'ccr', - controller: class CrossClusterReplicationController { - constructor($scope, $route) { - // React-router's does not play well with the angular router. It will cause this controller - // to re-execute without the $destroy handler being called. This means that the app will be mounted twice - // creating a memory leak when leaving (only 1 app will be unmounted). - // To avoid this, we unmount the React app each time we enter the controller. - unmountReactApp(); - - $scope.$$postDigest(() => { - elem = document.getElementById(CCR_REACT_ROOT); - renderReact(elem, npStart.core.i18n.Context); - - // Angular Lifecycle - const appRoute = $route.current; - const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template; - - // When we navigate within CCR, prevent Angular from re-matching the route and rebuild the app - if (isNavigationInApp) { - $route.current = appRoute; - } else { - // Any clean up when User leaves the CCR - } - - $scope.$on('$destroy', () => { - stopListeningForLocationChange && stopListeningForLocationChange(); - unmountReactApp(); - }); - }); - }); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js deleted file mode 100644 index 91527b8eb7cc5..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js +++ /dev/null @@ -1,198 +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. - */ - -export const elasticsearchJsPlugin = (Client, config, components) => { - const ca = components.clientAction.factory; - - Client.prototype.ccr = components.clientAction.namespaceFactory(); - const ccr = Client.prototype.ccr.prototype; - - ccr.permissions = ca({ - urls: [ - { - fmt: '/_security/user/_has_privileges', - }, - ], - needBody: true, - method: 'POST', - }); - - ccr.autoFollowPatterns = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow', - }, - ], - method: 'GET', - }); - - ccr.autoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.saveAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ccr.deleteAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'DELETE', - }); - - ccr.pauseAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>/pause', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.resumeAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>/resume', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.info = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/info', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.stats = ca({ - urls: [ - { - fmt: '/_ccr/stats', - }, - ], - method: 'GET', - }); - - ccr.followerIndexStats = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/stats', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.saveFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=name%>/_ccr/follow', - req: { - name: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ccr.pauseFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/pause_follow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.resumeFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/resume_follow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ccr.unfollowLeaderIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/unfollow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts deleted file mode 100644 index ae15073b979e1..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts +++ /dev/null @@ -1,36 +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 { APICaller } from 'src/core/server'; -import { Index } from '../../../../../plugins/index_management/server'; - -export const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => { - if (!indicesList?.length) { - return indicesList; - } - const params = { - path: '/_all/_ccr/info', - method: 'GET', - }; - try { - const { follower_indices: followerIndices } = await callWithRequest( - 'transport.request', - params - ); - return indicesList.map(index => { - const isFollowerIndex = !!followerIndices.find( - (followerIndex: { follower_index: string }) => { - return followerIndex.follower_index === index.name; - } - ); - return { - ...index, - isFollowerIndex, - }; - }); - } catch (e) { - return indicesList; - } -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts deleted file mode 100644 index 7a38d024d99a2..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts +++ /dev/null @@ -1,11 +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 { PluginInitializerContext } from 'src/core/server'; -import { CrossClusterReplicationServerPlugin } from './plugin'; - -export const plugin = (ctx: PluginInitializerContext) => - new CrossClusterReplicationServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap deleted file mode 100644 index 92ac6070904b5..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`[CCR] auto-follow stats serialization should deserialize auto-follow stats 1`] = ` -Object { - "autoFollowedClusters": Array [ - Object { - "clusterName": "new-york", - "lastSeenMetadataVersion": 15, - "timeSinceLastCheckMillis": 2426, - }, - ], - "numberOfFailedFollowIndices": 0, - "numberOfFailedRemoteClusterStateRequests": 0, - "numberOfSuccessfulFollowIndices": 0, - "recentAutoFollowErrors": Array [ - Object { - "autoFollowException": Object { - "reason": "index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]", - "type": "exception", - }, - "leaderIndex": "pattern-1:kibana_sample_1", - }, - Object { - "autoFollowException": Object { - "reason": "index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]", - "type": "exception", - }, - "leaderIndex": "pattern-2:kibana_sample_1", - }, - ], -} -`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index 99d72ce1a0e6e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,20 +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 { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../../client/elasticsearch_ccr'; - -const callWithRequest = once(server => { - const config = { plugins: [elasticsearchJsPlugin] }; - const cluster = server.plugins.elasticsearch.createCluster('ccr', config); - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js deleted file mode 100644 index e4d2f8d64d1bb..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js +++ /dev/null @@ -1,42 +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. - */ - -/* eslint-disable camelcase */ -export const deserializeRecentAutoFollowErrors = ({ - leader_index, - auto_follow_exception: { type, reason }, -}) => ({ - leaderIndex: leader_index, - autoFollowException: { - type, - reason, - }, -}); - -export const deserializeAutoFollowedClusters = ({ - cluster_name, - time_since_last_check_millis, - last_seen_metadata_version, -}) => ({ - clusterName: cluster_name, - timeSinceLastCheckMillis: time_since_last_check_millis, - lastSeenMetadataVersion: last_seen_metadata_version, -}); - -export const deserializeAutoFollowStats = ({ - number_of_failed_follow_indices, - number_of_failed_remote_cluster_state_requests, - number_of_successful_follow_indices, - recent_auto_follow_errors, - auto_followed_clusters, -}) => ({ - numberOfFailedFollowIndices: number_of_failed_follow_indices, - numberOfFailedRemoteClusterStateRequests: number_of_failed_remote_cluster_state_requests, - numberOfSuccessfulFollowIndices: number_of_successful_follow_indices, - recentAutoFollowErrors: recent_auto_follow_errors.map(deserializeRecentAutoFollowErrors), - autoFollowedClusters: auto_followed_clusters.map(deserializeAutoFollowedClusters), -}); -/* eslint-enable camelcase */ diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js deleted file mode 100644 index 5120c56701e5b..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js +++ /dev/null @@ -1,44 +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 { deserializeAutoFollowStats } from './ccr_stats_serialization'; - -describe('[CCR] auto-follow stats serialization', () => { - it('should deserialize auto-follow stats', () => { - const esObject = { - number_of_failed_follow_indices: 0, - number_of_failed_remote_cluster_state_requests: 0, - number_of_successful_follow_indices: 0, - recent_auto_follow_errors: [ - { - leader_index: 'pattern-1:kibana_sample_1', - auto_follow_exception: { - type: 'exception', - reason: - 'index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]', - }, - }, - { - leader_index: 'pattern-2:kibana_sample_1', - auto_follow_exception: { - type: 'exception', - reason: - 'index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]', - }, - }, - ], - auto_followed_clusters: [ - { - cluster_name: 'new-york', - time_since_last_check_millis: 2426, - last_seen_metadata_version: 15, - }, - ], - }; - - expect(deserializeAutoFollowStats(esObject)).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js deleted file mode 100644 index 6cf12896fa472..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js +++ /dev/null @@ -1,70 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - const pluginName = 'Cross-Cluster Replication'; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate( - 'xpack.crossClusterReplication.checkLicense.errorUnavailableMessage', - { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - } - ), - }; - } - - const VALID_LICENSE_MODES = ['trial', 'platinum', 'enterprise']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - isActive: false, - message: i18n.translate( - 'xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage', - { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - } - ), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - isAvailable: true, - isActive: false, - message: i18n.translate('xpack.crossClusterReplication.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid and active - return { - isAvailable: true, - isActive: true, - }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js deleted file mode 100644 index f2c070fd44b6e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { checkLicense } from './check_license'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js deleted file mode 100644 index 11a6fd4e1d816..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js +++ /dev/null @@ -1,33 +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 expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - originalError.response = '{}'; - }); - - it('should return the correct object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.statusCode).to.be(originalError.statusCode); - expect(wrappedError.message).to.be(originalError.message); - }); - - it('should return the correct object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.statusCode).to.be(originalError.statusCode); - expect(wrappedError.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts deleted file mode 100644 index 3756b0c74fb10..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { wrapEsError } from './wrap_es_error'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts deleted file mode 100644 index 8afd5f1a018eb..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts +++ /dev/null @@ -1,65 +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. - */ - -function extractCausedByChain( - causedBy: Record = {}, - accumulator: string[] = [] -): string[] { - const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase - - if (reason) { - accumulator.push(reason); - } - - // eslint-disable-next-line @typescript-eslint/camelcase - if (caused_by) { - return extractCausedByChain(caused_by, accumulator); - } - - return accumulator; -} - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - */ -export function wrapEsError( - err: any, - statusCodeToMessageMap: Record = {} -): { message: string; body?: { cause?: string[] }; statusCode: number } { - const { statusCode, response } = err; - - const { - error: { - root_cause = [], // eslint-disable-line @typescript-eslint/camelcase - caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase - } = {}, - } = JSON.parse(response); - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response and return it - if (!statusCodeToMessageMap[statusCode]) { - // The caused_by chain has the most information so use that if it's available. If not then - // settle for the root_cause. - const causedByChain = extractCausedByChain(caused_by); - const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; - - return { - message: err.message, - statusCode, - body: { - cause: causedByChain.length ? causedByChain : defaultCause, - }, - }; - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return { message, statusCode }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index 5f2141cce9395..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,44 +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 expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from 'lodash'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError, - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts deleted file mode 100644 index 441648a8701e0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { isEsErrorFactory } from './is_es_error_factory'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts deleted file mode 100644 index fc6405b8e7513..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts +++ /dev/null @@ -1,18 +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 { memoize } from 'lodash'; - -const esErrorsFactory = memoize((server: any) => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server: any) { - const esErrors = esErrorsFactory(server); - return function isEsError(err: any) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts deleted file mode 100644 index d22505f0e315a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts +++ /dev/null @@ -1,64 +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 { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockDeps: any; - let mockLicenseCheckResults: any; - - const anyContext: any = {}; - const anyRequest: any = {}; - - beforeEach(() => { - mockDeps = { - __LEGACY: { - server: { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }, - }, - requestHandler: jest.fn(), - }; - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', async () => { - const licensePreRouting = licensePreRoutingFactory(mockDeps); - const response = await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); - expect(response.status).toBe(403); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('it calls the wrapped handler', async () => { - const licensePreRouting = licensePreRoutingFactory(mockDeps); - await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); - expect(mockDeps.requestHandler).toHaveBeenCalledTimes(1); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts deleted file mode 100644 index 0743e443955f4..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index c47faa940a650..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ /dev/null @@ -1,32 +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 { RequestHandler } from 'src/core/server'; -import { PLUGIN } from '../../../../common/constants'; - -export const licensePreRoutingFactory = ({ - __LEGACY, - requestHandler, -}: { - __LEGACY: { server: any }; - requestHandler: RequestHandler; -}) => { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - - // License checking and enable/disable logic - const licensePreRouting: RequestHandler = (ctx, request, response) => { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - return response.forbidden({ - body: licenseCheckResults.message, - }); - } else { - return requestHandler(ctx, request, response); - } - }; - - return licensePreRouting; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js deleted file mode 100644 index b9bb34a80ce79..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,21 +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 { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; -import { PLUGIN } from '../../../../common/constants'; -import { checkLicense } from '../check_license'; - -export function registerLicenseChecker(__LEGACY) { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - const ccrPluggin = __LEGACY.server.plugins[PLUGIN.ID]; - - mirrorPluginStatus(xpackMainPlugin, ccrPluggin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts deleted file mode 100644 index 829de10ad0177..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts +++ /dev/null @@ -1,38 +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 { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server'; - -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/server'; - -// @ts-ignore -import { registerLicenseChecker } from './lib/register_license_checker'; -// @ts-ignore -import { registerRoutes } from './routes/register_routes'; -import { ccrDataEnricher } from './cross_cluster_replication_data'; - -interface PluginDependencies { - indexManagement: IndexManagementPluginSetup; - __LEGACY: { - server: any; - ccrUIEnabled: boolean; - }; -} - -export class CrossClusterReplicationServerPlugin implements Plugin { - // @ts-ignore - constructor(private readonly ctx: PluginInitializerContext) {} - setup({ http }: CoreSetup, { indexManagement, __LEGACY }: PluginDependencies) { - registerLicenseChecker(__LEGACY); - - const router = http.createRouter(); - registerRoutes({ router, __LEGACY }); - if (__LEGACY.ccrUIEnabled && indexManagement && indexManagement.indexDataEnricher) { - indexManagement.indexDataEnricher.add(ccrDataEnricher); - } - } - start() {} -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js deleted file mode 100644 index f3024515c7213..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js +++ /dev/null @@ -1,330 +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 { deserializeAutoFollowPattern } from '../../../../../common/services/auto_follow_pattern_serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../../../fixtures'; -import { registerAutoFollowPatternRoutes } from '../auto_follow_pattern'; - -import { createRouter, callRoute } from './helpers'; - -jest.mock('../../../lib/call_with_request_factory'); -jest.mock('../../../lib/is_es_error_factory'); -jest.mock('../../../lib/license_pre_routing_factory', () => ({ - licensePreRoutingFactory: ({ requestHandler }) => requestHandler, -})); - -const DESERIALIZED_KEYS = Object.keys(deserializeAutoFollowPattern(getAutoFollowPatternMock())); - -let routeRegistry; - -/** - * Helper to extract all the different server route handler so we can easily call them in our tests. - * - * Important: This method registers the handlers in the order that they appear in the file, so - * if a "server.route()" call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. - */ -const registerHandlers = () => { - const HANDLER_INDEX_TO_ACTION = { - 0: 'list', - 1: 'create', - 2: 'update', - 3: 'get', - 4: 'delete', - 5: 'pause', - 6: 'resume', - }; - - routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); - - registerAutoFollowPatternRoutes({ - __LEGACY: {}, - router: routeRegistry.router, - }); -}; - -/** - * Queue to save request response and errors - * It allows us to fake multiple responses from the - * callWithRequestFactory() when the request handler call it - * multiple times. - */ -let requestResponseQueue = []; - -/** - * Helper to mock the response from the call to Elasticsearch - * - * @param {*} err The mock error to throw - * @param {*} response The response to return - */ -const setHttpRequestResponse = (error, response) => { - requestResponseQueue.push({ error, response }); -}; - -const resetHttpRequestResponses = () => (requestResponseQueue = []); - -const getNextResponseFromQueue = () => { - if (!requestResponseQueue.length) { - return null; - } - - const next = requestResponseQueue.shift(); - if (next.error) { - return Promise.reject(next.error); - } - return Promise.resolve(next.response); -}; - -describe('[CCR API Routes] Auto Follow Pattern', () => { - let routeHandler; - - beforeAll(() => { - isEsErrorFactory.mockReturnValue(() => false); - callWithRequestFactory.mockReturnValue(getNextResponseFromQueue); - registerHandlers(); - }); - - describe('list()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().list; - }); - - it('should deserialize the response from Elasticsearch', async () => { - const totalResult = 2; - setHttpRequestResponse(null, getAutoFollowPatternListMock(totalResult)); - - const { - options: { body: response }, - } = await callRoute(routeHandler); - const autoFollowPattern = response.patterns[0]; - - expect(response.patterns.length).toEqual(totalResult); - expect(Object.keys(autoFollowPattern)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('create()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().create; - }); - - it('should throw a 409 conflict error if id already exists', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute( - routeHandler, - {}, - { - body: { - id: 'some-id', - foo: 'bar', - }, - } - ); - - expect(response.status).toEqual(409); - }); - - it('should return 200 status when the id does not exist', async () => { - const error = new Error('Resource not found.'); - error.statusCode = 404; - setHttpRequestResponse(error); - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute( - routeHandler, - {}, - { - body: { - id: 'some-id', - foo: 'bar', - }, - } - ); - - expect(response).toEqual({ acknowledge: true }); - }); - }); - - describe('update()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().update; - }); - - it('should serialize the payload before sending it to Elasticsearch', async () => { - callWithRequestFactory.mockReturnValueOnce((_, payload) => payload); - - const request = { - params: { id: 'foo' }, - body: { - remoteCluster: 'bar1', - leaderIndexPatterns: ['bar2'], - followIndexPattern: 'bar3', - }, - }; - - const response = await callRoute(routeHandler, {}, request); - - expect(response.options.body).toEqual({ - id: 'foo', - body: { - remote_cluster: 'bar1', - leader_index_patterns: ['bar2'], - follow_index_pattern: 'bar3', - }, - }); - }); - }); - - describe('get()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().get; - }); - - it('should return a single resource even though ES return an array with 1 item', async () => { - const autoFollowPattern = getAutoFollowPatternMock(); - const esResponse = { patterns: [autoFollowPattern] }; - - setHttpRequestResponse(null, esResponse); - - const response = await callRoute(routeHandler, {}, { params: { id: 1 } }); - expect(Object.keys(response.options.body)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('delete()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().delete; - }); - - it('should delete a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsDeleted).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to delete', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsDeleted).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsDeleted).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); - - describe('pause()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().pause; - }); - - it('accept a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsPaused).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of items to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsPaused).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsPaused).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); - - describe('resume()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().resume; - }); - - it('accept a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsResumed).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of items to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsResumed).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsResumed).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js deleted file mode 100644 index f0139e5bd7011..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js +++ /dev/null @@ -1,312 +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 { deserializeFollowerIndex } from '../../../../../common/services/follower_index_serialization'; -import { - getFollowerIndexStatsMock, - getFollowerIndexListStatsMock, - getFollowerIndexInfoMock, - getFollowerIndexListInfoMock, -} from '../../../../../fixtures'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { registerFollowerIndexRoutes } from '../follower_index'; -import { createRouter, callRoute } from './helpers'; - -jest.mock('../../../lib/call_with_request_factory'); -jest.mock('../../../lib/is_es_error_factory'); -jest.mock('../../../lib/license_pre_routing_factory', () => ({ - licensePreRoutingFactory: ({ requestHandler }) => requestHandler, -})); - -const DESERIALIZED_KEYS = Object.keys( - deserializeFollowerIndex({ - ...getFollowerIndexInfoMock(), - ...getFollowerIndexStatsMock(), - }) -); - -let routeRegistry; - -/** - * Helper to extract all the different server route handler so we can easily call them in our tests. - * - * Important: This method registers the handlers in the order that they appear in the file, so - * if a 'server.route()' call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. - */ -const registerHandlers = () => { - const HANDLER_INDEX_TO_ACTION = { - 0: 'list', - 1: 'get', - 2: 'create', - 3: 'edit', - 4: 'pause', - 5: 'resume', - 6: 'unfollow', - }; - - routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); - registerFollowerIndexRoutes({ - __LEGACY: {}, - router: routeRegistry.router, - }); -}; - -/** - * Queue to save request response and errors - * It allows us to fake multiple responses from the - * callWithRequestFactory() when the request handler call it - * multiple times. - */ -let requestResponseQueue = []; - -/** - * Helper to mock the response from the call to Elasticsearch - * - * @param {*} err The mock error to throw - * @param {*} response The response to return - */ -const setHttpRequestResponse = (error, response) => { - requestResponseQueue.push({ error, response }); -}; - -const resetHttpRequestResponses = () => (requestResponseQueue = []); - -const getNextResponseFromQueue = () => { - if (!requestResponseQueue.length) { - return null; - } - - const next = requestResponseQueue.shift(); - if (next.error) { - return Promise.reject(next.error); - } - return Promise.resolve(next.response); -}; - -describe('[CCR API Routes] Follower Index', () => { - let routeHandler; - - beforeAll(() => { - isEsErrorFactory.mockReturnValue(() => false); - callWithRequestFactory.mockReturnValue(getNextResponseFromQueue); - registerHandlers(); - }); - - describe('list()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().list; - }); - - it('deserializes the response from Elasticsearch', async () => { - const totalResult = 2; - const infoResult = getFollowerIndexListInfoMock(totalResult); - const statsResult = getFollowerIndexListStatsMock( - totalResult, - infoResult.follower_indices.map(index => index.follower_index) - ); - setHttpRequestResponse(null, infoResult); - setHttpRequestResponse(null, statsResult); - - const { - options: { body: response }, - } = await callRoute(routeHandler); - const followerIndex = response.indices[0]; - - expect(response.indices.length).toEqual(totalResult); - expect(Object.keys(followerIndex)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('get()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().get; - }); - - it('should return a single resource even though ES return an array with 1 item', async () => { - const mockId = 'test1'; - const followerIndexInfo = getFollowerIndexInfoMock(mockId); - const followerIndexStats = getFollowerIndexStatsMock(mockId); - - setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] }); - setHttpRequestResponse(null, { indices: [followerIndexStats] }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: mockId } }); - expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('create()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().create; - }); - - it('should return 200 status when follower index is created', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute( - routeHandler, - {}, - { - body: { - name: 'follower_index', - remoteCluster: 'remote_cluster', - leaderIndex: 'leader_index', - }, - } - ); - - expect(response.options.body).toEqual({ acknowledge: true }); - }); - }); - - describe('pause()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().pause; - }); - - it('should pause a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsPaused).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsPaused).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsPaused).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); - - describe('resume()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().resume; - }); - - it('should resume a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsResumed).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to resume', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsResumed).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsResumed).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); - - describe('unfollow()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().unfollow; - }); - - it('should unfollow await single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsUnfollowed).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to unfollow', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsUnfollowed).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsUnfollowed).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts deleted file mode 100644 index 555fc0937c0ad..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts +++ /dev/null @@ -1,37 +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 { RequestHandler } from 'src/core/server'; -import { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; - -export const callRoute = ( - route: RequestHandler, - ctx = {}, - request = {}, - response = kibanaResponseFactory -) => { - return route(ctx as any, request as any, response); -}; - -export const createRouter = (indexToActionMap: Record) => { - let index = 0; - const routeHandlers: Record> = {}; - const addHandler = (ignoreCtxForNow: any, handler: RequestHandler) => { - // Save handler and increment index - routeHandlers[indexToActionMap[index]] = handler; - index++; - }; - - return { - getRoutes: () => routeHandlers, - router: { - get: addHandler, - post: addHandler, - put: addHandler, - delete: addHandler, - }, - }; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts deleted file mode 100644 index d458f1ccb354b..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts +++ /dev/null @@ -1,301 +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 { schema } from '@kbn/config-schema'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -// @ts-ignore -import { - deserializeAutoFollowPattern, - deserializeListAutoFollowPatterns, - serializeAutoFollowPattern, - // @ts-ignore -} from '../../../../common/services/auto_follow_pattern_serialization'; - -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../../common/constants'; - -import { RouteDependencies } from '../types'; -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; - -export const registerAutoFollowPatternRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns a list of all auto-follow patterns - */ - router.get( - { - path: `${API_BASE_PATH}/auto_follow_patterns`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const result = await callWithRequest('ccr.autoFollowPatterns'); - return response.ok({ - body: { - patterns: deserializeListAutoFollowPatterns(result.patterns), - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Create an auto-follow pattern - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns`, - validate: { - body: schema.object( - { - id: schema.string(), - }, - { unknowns: 'allow' } - ), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id, ...rest } = request.body; - const body = serializeAutoFollowPattern(rest); - - /** - * First let's make sur that an auto-follow pattern with - * the same id does not exist. - */ - try { - await callWithRequest('ccr.autoFollowPattern', { id }); - // If we get here it means that an auto-follow pattern with the same id exists - return response.conflict({ - body: `An auto-follow pattern with the name "${id}" already exists.`, - }); - } catch (err) { - if (err.statusCode !== 404) { - return mapErrorToKibanaHttpResponse(err); - } - } - - try { - return response.ok({ - body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Update an auto-follow pattern - */ - router.put( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({}, { unknowns: 'allow' }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const body = serializeAutoFollowPattern(request.body); - - try { - return response.ok({ - body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns a single auto-follow pattern - */ - router.get( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - try { - const result = await callWithRequest('ccr.autoFollowPattern', { id }); - const autoFollowPattern = result.patterns[0]; - - return response.ok({ - body: deserializeAutoFollowPattern(autoFollowPattern), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Delete an auto-follow pattern - */ - router.delete( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsDeleted: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.deleteAutoFollowPattern', { id: _id }) - .then(() => itemsDeleted.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsDeleted, - errors, - }, - }); - }, - }) - ); - - /** - * Pause auto-follow pattern(s) - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseAutoFollowPattern', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsPaused, - errors, - }, - }); - }, - }) - ); - - /** - * Resume auto-follow pattern(s) - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeAutoFollowPattern', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsResumed, - errors, - }, - }); - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts deleted file mode 100644 index b08b056ad2c8a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts +++ /dev/null @@ -1,112 +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 { API_BASE_PATH } from '../../../../common/constants'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -// @ts-ignore -import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; -import { RouteDependencies } from '../types'; - -export const registerCcrRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns Auto-follow stats - */ - router.get( - { - path: `${API_BASE_PATH}/stats/auto_follow`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats'); - - return response.ok({ - body: deserializeAutoFollowStats(autoFollowStats), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns whether the user has CCR permissions - */ - router.get( - { - path: `${API_BASE_PATH}/permissions`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - const xpackInfo = xpackMainPlugin && xpackMainPlugin.info; - - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - return response.customError({ - statusCode: 503, - body: 'Security info unavailable', - }); - } - - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { - // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR. - return response.ok({ - body: { - hasPermission: true, - missingClusterPrivileges: [], - }, - }); - } - - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { has_all_requested: hasPermission, cluster } = await callWithRequest( - 'ccr.permissions', - { - body: { - cluster: ['manage', 'manage_ccr'], - }, - } - ); - - const missingClusterPrivileges = Object.keys(cluster).reduce( - (permissions: any, permissionName: any) => { - if (!cluster[permissionName]) { - permissions.push(permissionName); - return permissions; - } - }, - [] as any[] - ); - - return response.ok({ - body: { - hasPermission, - missingClusterPrivileges, - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts deleted file mode 100644 index 1d7dacf4a8688..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts +++ /dev/null @@ -1,357 +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 { schema } from '@kbn/config-schema'; -import { - deserializeFollowerIndex, - deserializeListFollowerIndices, - serializeFollowerIndex, - serializeAdvancedSettings, - // @ts-ignore -} from '../../../../common/services/follower_index_serialization'; -import { API_BASE_PATH } from '../../../../common/constants'; -// @ts-ignore -import { removeEmptyFields } from '../../../../common/services/utils'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -import { RouteDependencies } from '../types'; -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; - -export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns a list of all follower indices - */ - router.get( - { - path: `${API_BASE_PATH}/follower_indices`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { - id: '_all', - }); - - const { - follow_stats: { indices: followerIndicesStats }, - } = await callWithRequest('ccr.stats'); - - const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { - map[stats.index] = stats; - return map; - }, {}); - - const collatedFollowerIndices = followerIndices.map((followerIndex: any) => { - return { - ...followerIndex, - ...followerIndicesStatsMap[followerIndex.follower_index], - }; - }); - - return response.ok({ - body: { - indices: deserializeListFollowerIndices(collatedFollowerIndices), - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns a single follower index pattern - */ - router.get( - { - path: `${API_BASE_PATH}/follower_indices/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - - const followerIndexInfo = followerIndices && followerIndices[0]; - - if (!followerIndexInfo) { - return response.notFound({ - body: `The follower index "${id}" does not exist.`, - }); - } - - // If this follower is paused, skip call to ES stats api since it will return 404 - if (followerIndexInfo.status === 'paused') { - return response.ok({ - body: deserializeFollowerIndex({ - ...followerIndexInfo, - }), - }); - } else { - const { - indices: followerIndicesStats, - } = await callWithRequest('ccr.followerIndexStats', { id }); - - return response.ok({ - body: deserializeFollowerIndex({ - ...followerIndexInfo, - ...(followerIndicesStats ? followerIndicesStats[0] : {}), - }), - }); - } - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Create a follower index - */ - router.post( - { - path: `${API_BASE_PATH}/follower_indices`, - validate: { - body: schema.object( - { - name: schema.string(), - }, - { unknowns: 'allow' } - ), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { name, ...rest } = request.body; - const body = removeEmptyFields(serializeFollowerIndex(rest)); - - try { - return response.ok({ - body: await callWithRequest('ccr.saveFollowerIndex', { name, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Edit a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}`, - validate: { - params: schema.object({ id: schema.string() }), - body: schema.object({ - maxReadRequestOperationCount: schema.maybe(schema.number()), - maxOutstandingReadRequests: schema.maybe(schema.number()), - maxReadRequestSize: schema.maybe(schema.string()), // byte value - maxWriteRequestOperationCount: schema.maybe(schema.number()), - maxWriteRequestSize: schema.maybe(schema.string()), // byte value - maxOutstandingWriteRequests: schema.maybe(schema.number()), - maxWriteBufferCount: schema.maybe(schema.number()), - maxWriteBufferSize: schema.maybe(schema.string()), // byte value - maxRetryDelay: schema.maybe(schema.string()), // time value - readPollTimeout: schema.maybe(schema.string()), // time value - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - // We need to first pause the follower and then resume it passing the advanced settings - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - const followerIndexInfo = followerIndices && followerIndices[0]; - if (!followerIndexInfo) { - return response.notFound({ body: `The follower index "${id}" does not exist.` }); - } - - // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. - const isPaused = followerIndexInfo.status === 'paused'; - // Pause follower if not already paused - if (!isPaused) { - await callWithRequest('ccr.pauseFollowerIndex', { id }); - } - - // Resume follower - const body = removeEmptyFields(serializeAdvancedSettings(request.body)); - return response.ok({ - body: await callWithRequest('ccr.resumeFollowerIndex', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Pauses a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/pause`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseFollowerIndex', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch((err: Error) => { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - }) - ) - ); - - return response.ok({ - body: { - itemsPaused, - errors, - }, - }); - }, - }) - ); - - /** - * Resumes a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/resume`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeFollowerIndex', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch((err: Error) => { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - }) - ) - ); - - return response.ok({ - body: { - itemsResumed, - errors, - }, - }); - }, - }) - ); - - /** - * Unfollow follower index's leader index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsUnfollowed: string[] = []; - const itemsNotOpen: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(async _id => { - try { - // Try to pause follower, let it fail silently since it may already be paused - try { - await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); - } catch (e) { - // Swallow errors - } - - // Close index - await callWithRequest('indices.close', { index: _id }); - - // Unfollow leader - await callWithRequest('ccr.unfollowLeaderIndex', { id: _id }); - - // Try to re-open the index, store failures in a separate array to surface warnings in the UI - // This will allow users to query their index normally after unfollowing - try { - await callWithRequest('indices.open', { index: _id }); - } catch (e) { - itemsNotOpen.push(_id); - } - - // Push success - itemsUnfollowed.push(_id); - } catch (err) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ); - - return response.ok({ - body: { - itemsUnfollowed, - itemsNotOpen, - errors, - }, - }); - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts deleted file mode 100644 index 6a81bd26dc47d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts +++ /dev/null @@ -1,26 +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 { kibanaResponseFactory } from '../../../../../../../src/core/server'; -// @ts-ignore -import { wrapEsError } from '../lib/error_wrappers'; -import { isEsError } from '../lib/is_es_error'; - -export const mapErrorToKibanaHttpResponse = (err: any) => { - if (isEsError(err)) { - const { statusCode, message, body } = wrapEsError(err); - return kibanaResponseFactory.customError({ - statusCode, - body: { - message, - attributes: { - cause: body?.cause, - }, - }, - }); - } - return kibanaResponseFactory.internalError(err); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts deleted file mode 100644 index 7e59417550691..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts +++ /dev/null @@ -1,16 +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 { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern'; -import { registerFollowerIndexRoutes } from './api/follower_index'; -import { registerCcrRoutes } from './api/ccr'; -import { RouteDependencies } from './types'; - -export function registerRoutes(deps: RouteDependencies) { - registerAutoFollowPatternRoutes(deps); - registerFollowerIndexRoutes(deps); - registerCcrRoutes(deps); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts deleted file mode 100644 index 7f57c20c536e0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts +++ /dev/null @@ -1,13 +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 { IRouter } from 'src/core/server'; - -export interface RouteDependencies { - router: IRouter; - __LEGACY: { - server: any; - }; -} diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts deleted file mode 100644 index 5c7f8fa46c18b..0000000000000 --- a/x-pack/legacy/plugins/graph/index.ts +++ /dev/null @@ -1,32 +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. - */ - -// @ts-ignore -import migrations from './migrations'; -import mappings from './mappings.json'; -import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; - -export const graph: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - id: 'graph', - configPrefix: 'xpack.graph', - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - mappings, - migrations, - }, - - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - canEditDrillDownUrls: Joi.boolean().default(true), - savePolicy: Joi.string() - .valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']) - .default('configAndData'), - }).default(); - }, - }); -}; diff --git a/x-pack/legacy/plugins/graph/mappings.json b/x-pack/legacy/plugins/graph/mappings.json deleted file mode 100644 index f1950c459eee5..0000000000000 --- a/x-pack/legacy/plugins/graph/mappings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - } -} diff --git a/x-pack/legacy/plugins/graph/migrations.js b/x-pack/legacy/plugins/graph/migrations.js deleted file mode 100644 index 0cefe6217b45d..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.js +++ /dev/null @@ -1,41 +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 { get } from 'lodash'; - -export default { - 'graph-workspace': { - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - // Migrate index pattern - const wsState = get(doc, 'attributes.wsState'); - if (typeof wsState !== 'string') { - return doc; - } - let state; - try { - state = JSON.parse(JSON.parse(wsState)); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc; - } - const { indexPattern } = state; - if (!indexPattern) { - return doc; - } - state.indexPatternRefName = 'indexPattern_0'; - delete state.indexPattern; - doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); - doc.references.push({ - name: 'indexPattern_0', - type: 'index-pattern', - id: indexPattern, - }); - return doc; - }, - }, -}; diff --git a/x-pack/legacy/plugins/graph/migrations.test.js b/x-pack/legacy/plugins/graph/migrations.test.js deleted file mode 100644 index 93162d94857ce..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.test.js +++ /dev/null @@ -1,102 +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 migrations from './migrations'; - -describe('graph-workspace', () => { - describe('7.0.0', () => { - const migration = migrations['graph-workspace']['7.0.0']; - - test('returns doc on empty object', () => { - expect(migration({})).toMatchInlineSnapshot(` -Object { - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not a string', () => { - const doc = { - id: '1', - attributes: { - wsState: true, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": true, - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not valid JSON', () => { - const doc = { - id: '1', - attributes: { - wsState: '123abc', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "123abc", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when "indexPattern" is missing from wsState', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true })), - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('extract "indexPattern" attribute from doc', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), - bar: true, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "bar": true, - "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "indexPattern_0", - "type": "index-pattern", - }, - ], -} -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/README.md b/x-pack/legacy/plugins/logstash/README.md deleted file mode 100755 index 7d181249300fa..0000000000000 --- a/x-pack/legacy/plugins/logstash/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Logstash Plugin - -This plugin adds Logstash specific UI code to x-pack. Currently this plugin adds just the management features. diff --git a/x-pack/legacy/plugins/logstash/common/lib/__tests__/get_moment.js b/x-pack/legacy/plugins/logstash/common/lib/__tests__/get_moment.js deleted file mode 100755 index 2e63b231bec32..0000000000000 --- a/x-pack/legacy/plugins/logstash/common/lib/__tests__/get_moment.js +++ /dev/null @@ -1,33 +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 expect from '@kbn/expect'; -import { getMoment } from '../get_moment'; - -describe('get_moment', () => { - describe('getMoment', () => { - it(`returns a moment object when passed a date`, () => { - const moment = getMoment('2017-03-30T14:53:08.121Z'); - - expect(moment.constructor.name).to.be('Moment'); - }); - - it(`returns null when passed falsy`, () => { - const results = [ - getMoment(false), - getMoment(0), - getMoment(''), - getMoment(null), - getMoment(undefined), - getMoment(NaN), - ]; - - results.forEach(result => { - expect(result).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/common/lib/get_moment.js b/x-pack/legacy/plugins/logstash/common/lib/get_moment.js deleted file mode 100755 index 7a808ec9a0336..0000000000000 --- a/x-pack/legacy/plugins/logstash/common/lib/get_moment.js +++ /dev/null @@ -1,15 +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 moment from 'moment'; - -export function getMoment(date) { - if (!date) { - return null; - } - - return moment(date); -} diff --git a/x-pack/legacy/plugins/logstash/common/lib/index.js b/x-pack/legacy/plugins/logstash/common/lib/index.js deleted file mode 100755 index 6ed1d24a37791..0000000000000 --- a/x-pack/legacy/plugins/logstash/common/lib/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { getMoment } from './get_moment'; diff --git a/x-pack/legacy/plugins/logstash/index.js b/x-pack/legacy/plugins/logstash/index.js deleted file mode 100755 index 29f01032f3413..0000000000000 --- a/x-pack/legacy/plugins/logstash/index.js +++ /dev/null @@ -1,32 +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 { resolve } from 'path'; -import { registerLicenseChecker } from './server/lib/register_license_checker'; -import { PLUGIN } from '../../../plugins/logstash/common/constants'; - -export const logstash = kibana => - new kibana.Plugin({ - id: PLUGIN.ID, - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - configPrefix: 'xpack.logstash', - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - uiExports: { - managementSections: [ - 'plugins/logstash/sections/pipeline_list', - 'plugins/logstash/sections/pipeline_edit', - ], - home: ['plugins/logstash/lib/register_home_feature'], - }, - init: server => { - registerLicenseChecker(server); - }, - }); diff --git a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts b/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts deleted file mode 100644 index 2e1ee2afb9ce6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.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 { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -// @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; -// @ts-ignore -import { PLUGIN } from '../../../../../plugins/logstash/common/constants'; - -const { - plugins: { home }, -} = npSetup; - -const enableLinks = Boolean(xpackInfo.get(`features.${PLUGIN.ID}.enableLinks`)); - -if (enableLinks) { - home.featureCatalogue.register({ - id: 'management_logstash', - title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { - defaultMessage: 'Logstash Pipelines', - }), - description: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesDescription', { - defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', - }), - icon: 'pipelineApp', - path: '/app/kibana#/management/logstash/pipelines', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }); -} diff --git a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/index.js b/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/index.js deleted file mode 100755 index 9d53d4dd61163..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { updateLogstashSections } from './update_logstash_sections'; diff --git a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js b/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js deleted file mode 100755 index 8da687529c846..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js +++ /dev/null @@ -1,22 +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 { management } from 'ui/management'; - -export function updateLogstashSections(pipelineId) { - const editSection = management.getSection('logstash/pipelines/pipeline/edit'); - const newSection = management.getSection('logstash/pipelines/pipeline/new'); - - newSection.hide(); - editSection.hide(); - - if (pipelineId) { - editSection.url = `#/management/logstash/pipelines/pipeline/${pipelineId}/edit`; - editSection.show(); - } else { - newSection.show(); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js deleted file mode 100755 index 5889bbdf96a93..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js +++ /dev/null @@ -1,7 +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 './pipeline_edit'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js deleted file mode 100755 index 83446278fdeca..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js +++ /dev/null @@ -1,60 +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 React from 'react'; -import { render } from 'react-dom'; -import { isEmpty } from 'lodash'; -import { uiModules } from 'ui/modules'; -import { npSetup } from 'ui/new_platform'; -import { toastNotifications } from 'ui/notify'; -import { I18nContext } from 'ui/i18n'; -import { PipelineEditor } from '../../../../components/pipeline_editor'; -import 'plugins/logstash/services/license'; -import { logstashSecurity } from 'plugins/logstash/services/security'; -import 'ace'; - -const app = uiModules.get('xpack/logstash'); - -app.directive('pipelineEdit', function($injector) { - const pipelineService = $injector.get('pipelineService'); - const licenseService = $injector.get('logstashLicenseService'); - const kbnUrl = $injector.get('kbnUrl'); - const $route = $injector.get('$route'); - - return { - restrict: 'E', - link: async (scope, el) => { - const close = () => scope.$evalAsync(kbnUrl.change('/management/logstash/pipelines', {})); - const open = id => - scope.$evalAsync(kbnUrl.change(`/management/logstash/pipelines/${id}/edit`)); - - const userResource = logstashSecurity.isSecurityEnabled() - ? await npSetup.plugins.security.authc.getCurrentUser() - : null; - - render( - - - , - el[0] - ); - }, - scope: { - pipeline: '=', - }, - }; -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js deleted file mode 100755 index 3a9a6b860c51f..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js +++ /dev/null @@ -1,7 +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 './upgrade_failure'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js deleted file mode 100755 index 2ef99d3b47672..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js +++ /dev/null @@ -1,49 +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 React from 'react'; -import { render } from 'react-dom'; -import { isEmpty } from 'lodash'; -import { uiModules } from 'ui/modules'; -import { I18nContext } from 'ui/i18n'; -import { UpgradeFailure } from '../../../../components/upgrade_failure'; - -const app = uiModules.get('xpack/logstash'); - -app.directive('upgradeFailure', $injector => { - const $route = $injector.get('$route'); - const kbnUrl = $injector.get('kbnUrl'); - - return { - link: (scope, el) => { - const onRetry = () => { - $route.updateParams({ retry: true }); - $route.reload(); - }; - const onClose = () => { - scope.$evalAsync(kbnUrl.change('management/logstash/pipelines', {})); - }; - const isNewPipeline = isEmpty(scope.pipeline.id); - const isManualUpgrade = !!$route.current.params.retry; - - render( - - - , - el[0] - ); - }, - restrict: 'E', - scope: { - pipeline: '=', - }, - }; -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/index.js deleted file mode 100755 index 4b699ed79cd26..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/index.js +++ /dev/null @@ -1,7 +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 './pipeline_edit_route'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html deleted file mode 100755 index e1c422d46dfdb..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js deleted file mode 100755 index 733f7dc3ae2e6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js +++ /dev/null @@ -1,87 +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 routes from 'ui/routes'; -import { toastNotifications } from 'ui/notify'; -import { i18n } from '@kbn/i18n'; -import template from './pipeline_edit_route.html'; -import 'plugins/logstash/services/pipeline'; -import 'plugins/logstash/services/license'; -import 'plugins/logstash/services/upgrade'; -import './components/pipeline_edit'; -import './components/upgrade_failure'; -import { updateLogstashSections } from 'plugins/logstash/lib/update_management_sections'; -import { Pipeline } from 'plugins/logstash/models/pipeline'; -import { getPipelineCreateBreadcrumbs, getPipelineEditBreadcrumbs } from '../breadcrumbs'; - -routes - .when('/management/logstash/pipelines/pipeline/:id/edit', { - k7Breadcrumbs: getPipelineEditBreadcrumbs, - }) - .when('/management/logstash/pipelines/new-pipeline', { - k7Breadcrumbs: getPipelineCreateBreadcrumbs, - }) - .defaults(/management\/logstash\/pipelines\/(new-pipeline|pipeline\/:id\/edit)/, { - template: template, - controller: class PipelineEditRouteController { - constructor($injector) { - const $route = $injector.get('$route'); - this.pipeline = $route.current.locals.pipeline; - this.isUpgraded = $route.current.locals.isUpgraded; - } - }, - controllerAs: 'pipelineEditRoute', - resolve: { - logstashTabs: $injector => { - const $route = $injector.get('$route'); - const pipelineId = $route.current.params.id; - updateLogstashSections(pipelineId); - }, - pipeline: function($injector) { - const $route = $injector.get('$route'); - const pipelineService = $injector.get('pipelineService'); - const licenseService = $injector.get('logstashLicenseService'); - const kbnUrl = $injector.get('kbnUrl'); - - const pipelineId = $route.current.params.id; - - if (!pipelineId) return new Pipeline(); - - return pipelineService - .loadPipeline(pipelineId) - .then(pipeline => (!!$route.current.params.clone ? pipeline.clone : pipeline)) - .catch(err => { - return licenseService.checkValidity().then(() => { - if (err.status !== 403) { - toastNotifications.addDanger( - i18n.translate('xpack.logstash.couldNotLoadPipelineErrorNotification', { - defaultMessage: `Couldn't load pipeline. Error: '{errStatusText}'.`, - values: { - errStatusText: err.statusText, - }, - }) - ); - } - - kbnUrl.redirect('/management/logstash/pipelines'); - return Promise.reject(); - }); - }); - }, - checkLicense: $injector => { - const licenseService = $injector.get('logstashLicenseService'); - return licenseService.checkValidity(); - }, - isUpgraded: $injector => { - const upgradeService = $injector.get('upgradeService'); - return upgradeService.executeUpgrade(); - }, - }, - }); - -routes.when('/management/logstash/pipelines/pipeline/:id', { - redirectTo: '/management/logstash/pipelines/pipeline/:id/edit', -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js deleted file mode 100755 index 7e8ca0e4c2c57..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js +++ /dev/null @@ -1,7 +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 './pipeline_list'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js deleted file mode 100755 index b856979aed8b6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js +++ /dev/null @@ -1,58 +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 React from 'react'; -import { render } from 'react-dom'; -import { uiModules } from 'ui/modules'; -import { toastNotifications } from 'ui/notify'; -import { I18nContext } from 'ui/i18n'; -import { PipelineList } from '../../../../components/pipeline_list'; -import 'plugins/logstash/services/pipelines'; -import 'plugins/logstash/services/license'; -import 'plugins/logstash/services/cluster'; -import 'plugins/logstash/services/monitoring'; - -const app = uiModules.get('xpack/logstash'); - -app.directive('pipelineList', function($injector) { - const pipelinesService = $injector.get('pipelinesService'); - const licenseService = $injector.get('logstashLicenseService'); - const clusterService = $injector.get('xpackLogstashClusterService'); - const monitoringService = $injector.get('xpackLogstashMonitoringService'); - const kbnUrl = $injector.get('kbnUrl'); - - return { - restrict: 'E', - link: (scope, el) => { - const openPipeline = id => - scope.$evalAsync(kbnUrl.change(`management/logstash/pipelines/pipeline/${id}/edit`)); - const createPipeline = () => - scope.$evalAsync(kbnUrl.change('management/logstash/pipelines/new-pipeline')); - const clonePipeline = id => - scope.$evalAsync(kbnUrl.change(`management/logstash/pipelines/pipeline/${id}/edit?clone`)); - render( - - - , - el[0] - ); - }, - scope: {}, - controllerAs: 'pipelineList', - }; -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/index.js deleted file mode 100755 index f60decd1378d5..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/index.js +++ /dev/null @@ -1,8 +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 './register_management_section'; -import './pipeline_list_route'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html deleted file mode 100755 index 55b3fabd70161..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js deleted file mode 100755 index eb593207572c2..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js +++ /dev/null @@ -1,33 +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 routes from 'ui/routes'; -import { management } from 'ui/management'; -import template from './pipeline_list_route.html'; -import './components/pipeline_list'; -import 'plugins/logstash/services/license'; -import { getPipelineListBreadcrumbs } from '../breadcrumbs'; - -routes.when('/management/logstash/pipelines/', { - template, - k7Breadcrumbs: getPipelineListBreadcrumbs, -}); - -routes.defaults(/\/management/, { - resolve: { - logstashManagementSection: $injector => { - const licenseService = $injector.get('logstashLicenseService'); - const logstashSection = management.getSection('logstash/pipelines'); - - if (licenseService.enableLinks) { - logstashSection.show(); - logstashSection.enable(); - } else { - logstashSection.hide(); - } - }, - }, -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/register_management_section.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/register_management_section.js deleted file mode 100755 index e285418f5f2ae..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/register_management_section.js +++ /dev/null @@ -1,36 +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 { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; - -management.getSection('logstash').register('pipelines', { - display: i18n.translate('xpack.logstash.managementSection.pipelinesTitle', { - defaultMessage: 'Pipelines', - }), - order: 10, - url: '#/management/logstash/pipelines/', -}); - -management.getSection('logstash/pipelines').register('pipeline', { - visible: false, -}); - -management.getSection('logstash/pipelines/pipeline').register('edit', { - display: i18n.translate('xpack.logstash.managementSection.editPipelineTitle', { - defaultMessage: 'Edit pipeline', - }), - order: 1, - visible: false, -}); - -management.getSection('logstash/pipelines/pipeline').register('new', { - display: i18n.translate('xpack.logstash.managementSection.createPipelineTitle', { - defaultMessage: 'Create pipeline', - }), - order: 1, - visible: false, -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.factory.js deleted file mode 100755 index 0fee2804c704d..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.factory.js +++ /dev/null @@ -1,13 +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 { uiModules } from 'ui/modules'; -import { ClusterService } from './cluster_service'; - -uiModules.get('xpack/logstash').factory('xpackLogstashClusterService', $injector => { - const $http = $injector.get('$http'); - return new ClusterService($http); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js b/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js deleted file mode 100755 index e89c2fe7d11bf..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js +++ /dev/null @@ -1,31 +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 chrome from 'ui/chrome'; -import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; -import { Cluster } from 'plugins/logstash/models/cluster'; - -export class ClusterService { - constructor($http) { - this.$http = $http; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - loadCluster() { - return this.$http.get(`${this.basePath}/cluster`).then(response => { - if (!response.data) { - return; - } - return Cluster.fromUpstreamJSON(response.data.cluster); - }); - } - - isClusterInfoAvailable() { - return this.loadCluster() - .then(cluster => Boolean(cluster)) - .catch(() => false); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/cluster/index.js b/x-pack/legacy/plugins/logstash/public/services/cluster/index.js deleted file mode 100755 index ba52657a27ca8..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/cluster/index.js +++ /dev/null @@ -1,7 +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 './cluster_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/license/index.js b/x-pack/legacy/plugins/logstash/public/services/license/index.js deleted file mode 100755 index 8be8fb5ccbc64..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/license/index.js +++ /dev/null @@ -1,7 +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 './license_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/license/license_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/license/license_service.factory.js deleted file mode 100755 index 0e131f9b94008..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/license/license_service.factory.js +++ /dev/null @@ -1,14 +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 { uiModules } from 'ui/modules'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import 'ui/url'; -import { LogstashLicenseService } from './logstash_license_service'; - -uiModules.get('xpack/logstash').factory('logstashLicenseService', ($timeout, kbnUrl) => { - return new LogstashLicenseService(xpackInfo, kbnUrl, $timeout); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js b/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js deleted file mode 100755 index 69cc8614a6ae2..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js +++ /dev/null @@ -1,62 +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 React from 'react'; -import { toastNotifications } from 'ui/notify'; -import { MarkdownSimple } from '../../../../../../../src/plugins/kibana_react/public'; -import { PLUGIN } from '../../../../../../plugins/logstash/common/constants'; - -export class LogstashLicenseService { - constructor(xpackInfoService, kbnUrlService, $timeout) { - this.xpackInfoService = xpackInfoService; - this.kbnUrlService = kbnUrlService; - this.$timeout = $timeout; - } - - get enableLinks() { - return Boolean(this.xpackInfoService.get(`features.${PLUGIN.ID}.enableLinks`)); - } - - get isAvailable() { - return Boolean(this.xpackInfoService.get(`features.${PLUGIN.ID}.isAvailable`)); - } - - get isReadOnly() { - return Boolean(this.xpackInfoService.get(`features.${PLUGIN.ID}.isReadOnly`)); - } - - get message() { - return this.xpackInfoService.get(`features.${PLUGIN.ID}.message`); - } - - notifyAndRedirect() { - toastNotifications.addDanger({ - title: ( - - {this.xpackInfoService.get(`features.${PLUGIN.ID}.message`)} - - ), - }); - this.kbnUrlService.redirect('/management'); - } - - /** - * Checks if the license is valid or the license can perform downgraded UI tasks. - * Otherwise, notifies and redirects. - */ - checkValidity() { - return new Promise((resolve, reject) => { - this.$timeout(() => { - if (this.isAvailable) { - return resolve(); - } - - this.notifyAndRedirect(); - return reject(); - }, 10); // To allow latest XHR call to update license info - }); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/monitoring/index.js b/x-pack/legacy/plugins/logstash/public/services/monitoring/index.js deleted file mode 100755 index 83b2105beb5ef..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/monitoring/index.js +++ /dev/null @@ -1,7 +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 './monitoring_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.factory.js deleted file mode 100755 index 271c776dd6f69..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.factory.js +++ /dev/null @@ -1,18 +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 { uiModules } from 'ui/modules'; -import { MonitoringService } from './monitoring_service'; -import '../cluster'; - -uiModules.get('xpack/logstash').factory('xpackLogstashMonitoringService', $injector => { - const $http = $injector.get('$http'); - const Promise = $injector.get('Promise'); - const monitoringUiEnabled = - $injector.has('monitoringUiEnabled') && $injector.get('monitoringUiEnabled'); - const clusterService = $injector.get('xpackLogstashClusterService'); - return new MonitoringService($http, Promise, monitoringUiEnabled, clusterService); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js b/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js deleted file mode 100755 index 6103e730c2171..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js +++ /dev/null @@ -1,48 +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 moment from 'moment'; -import chrome from 'ui/chrome'; -import { ROUTES, MONITORING } from '../../../../../../plugins/logstash/common/constants'; -import { PipelineListItem } from 'plugins/logstash/models/pipeline_list_item'; - -export class MonitoringService { - constructor($http, Promise, monitoringUiEnabled, clusterService) { - this.$http = $http; - this.Promise = Promise; - this.monitoringUiEnabled = monitoringUiEnabled; - this.clusterService = clusterService; - this.basePath = chrome.addBasePath(ROUTES.MONITORING_API_ROOT); - } - - isMonitoringEnabled() { - return this.monitoringUiEnabled; - } - - getPipelineList() { - if (!this.isMonitoringEnabled()) { - return Promise.resolve([]); - } - - return this.clusterService - .loadCluster() - .then(cluster => { - const url = `${this.basePath}/v1/clusters/${cluster.uuid}/logstash/pipeline_ids`; - const now = moment.utc(); - const body = { - timeRange: { - max: now.toISOString(), - min: now.subtract(MONITORING.ACTIVE_PIPELINE_RANGE_S, 'seconds').toISOString(), - }, - }; - return this.$http.post(url, body); - }) - .then(response => - response.data.map(pipeline => PipelineListItem.fromUpstreamMonitoringJSON(pipeline)) - ) - .catch(() => []); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/pipeline/index.js b/x-pack/legacy/plugins/logstash/public/services/pipeline/index.js deleted file mode 100755 index 3b0e28bd555e6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipeline/index.js +++ /dev/null @@ -1,7 +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 './pipeline_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.factory.js deleted file mode 100755 index cf93915425213..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.factory.js +++ /dev/null @@ -1,14 +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 { uiModules } from 'ui/modules'; -import { PipelineService } from './pipeline_service'; - -uiModules.get('xpack/logstash').factory('pipelineService', $injector => { - const $http = $injector.get('$http'); - const pipelinesService = $injector.get('pipelinesService'); - return new PipelineService($http, pipelinesService); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js b/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js deleted file mode 100755 index b5d0dbeb852d5..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js +++ /dev/null @@ -1,40 +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 chrome from 'ui/chrome'; -import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; -import { Pipeline } from 'plugins/logstash/models/pipeline'; - -export class PipelineService { - constructor($http, pipelinesService) { - this.$http = $http; - this.pipelinesService = pipelinesService; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - loadPipeline(id) { - return this.$http.get(`${this.basePath}/pipeline/${id}`).then(response => { - return Pipeline.fromUpstreamJSON(response.data); - }); - } - - savePipeline(pipelineModel) { - return this.$http - .put(`${this.basePath}/pipeline/${pipelineModel.id}`, pipelineModel.upstreamJSON) - .catch(e => { - throw e.data.message; - }); - } - - deletePipeline(id) { - return this.$http - .delete(`${this.basePath}/pipeline/${id}`) - .then(() => this.pipelinesService.addToRecentlyDeleted(id)) - .catch(e => { - throw e.data.message; - }); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/pipelines/index.js b/x-pack/legacy/plugins/logstash/public/services/pipelines/index.js deleted file mode 100755 index e273e12d46c6d..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipelines/index.js +++ /dev/null @@ -1,7 +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 './pipelines_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.factory.js deleted file mode 100755 index 9295949e001eb..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.factory.js +++ /dev/null @@ -1,17 +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 { uiModules } from 'ui/modules'; -import { PipelinesService } from './pipelines_service'; -import '../monitoring'; - -uiModules.get('xpack/logstash').factory('pipelinesService', $injector => { - const $http = $injector.get('$http'); - const $window = $injector.get('$window'); - const Promise = $injector.get('Promise'); - const monitoringService = $injector.get('xpackLogstashMonitoringService'); - return new PipelinesService($http, $window, Promise, monitoringService); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js b/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js deleted file mode 100755 index d70c8be06fde4..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js +++ /dev/null @@ -1,135 +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 chrome from 'ui/chrome'; -import { ROUTES, MONITORING } from '../../../../../../plugins/logstash/common/constants'; -import { PipelineListItem } from 'plugins/logstash/models/pipeline_list_item'; - -const RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY = 'xpack.logstash.recentlyDeletedPipelines'; - -export class PipelinesService { - constructor($http, $window, Promise, monitoringService) { - this.$http = $http; - this.$window = $window; - this.Promise = Promise; - this.monitoringService = monitoringService; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - getPipelineList() { - return this.Promise.all([ - this.getManagementPipelineList(), - this.getMonitoringPipelineList(), - ]).then(([managementPipelines, monitoringPipelines]) => { - const now = Date.now(); - - // Monitoring will report centrally-managed pipelines as well, including recently-deleted centrally-managed ones. - // If there's a recently-deleted pipeline we're keeping track of BUT monitoring doesn't report it, that means - // it's not running in Logstash any more. So we can stop tracking it as a recently-deleted pipeline. - const monitoringPipelineIds = monitoringPipelines.map(pipeline => pipeline.id); - this.getRecentlyDeleted().forEach(recentlyDeletedPipeline => { - // We don't want to stop tracking the recently-deleted pipeline until Monitoring has had some - // time to report on it. Otherwise, if we stop tracking first, *then* Monitoring reports it, we'll - // still end up showing it in the list until Monitoring stops reporting it. - if (now - recentlyDeletedPipeline.deletedOn < MONITORING.ACTIVE_PIPELINE_RANGE_S * 1000) { - return; - } - - // If Monitoring is still reporting the pipeline, don't stop tracking it yet - if (monitoringPipelineIds.includes(recentlyDeletedPipeline.id)) { - return; - } - - this.removeFromRecentlyDeleted(recentlyDeletedPipeline.id); - }); - - // Merge centrally-managed pipelines with pipelines reported by monitoring. Take care to dedupe - // while merging because monitoring will (rightly) report centrally-managed pipelines as well, - // including recently-deleted ones! - const managementPipelineIds = managementPipelines.map(pipeline => pipeline.id); - return managementPipelines.concat( - monitoringPipelines.filter( - monitoringPipeline => - !managementPipelineIds.includes(monitoringPipeline.id) && - !this.isRecentlyDeleted(monitoringPipeline.id) - ) - ); - }); - } - - getManagementPipelineList() { - return this.$http - .get(`${this.basePath}/pipelines`) - .then(response => - response.data.pipelines.map(pipeline => PipelineListItem.fromUpstreamJSON(pipeline)) - ); - } - - getMonitoringPipelineList() { - return this.monitoringService.getPipelineList(); - } - - /** - * Delete a collection of pipelines - * - * @param pipelineIds Array of pipeline IDs - * @return Promise { numSuccesses, numErrors } - */ - deletePipelines(pipelineIds) { - const body = { - pipelineIds, - }; - return this.$http.post(`${this.basePath}/pipelines/delete`, body).then(response => { - this.addToRecentlyDeleted(...pipelineIds); - return response.data.results; - }); - } - - addToRecentlyDeleted(...pipelineIds) { - const recentlyDeletedPipelines = this.getRecentlyDeleted(); - const recentlyDeletedPipelineIds = recentlyDeletedPipelines.map(pipeline => pipeline.id); - pipelineIds.forEach(pipelineId => { - if (!recentlyDeletedPipelineIds.includes(pipelineId)) { - recentlyDeletedPipelines.push({ - id: pipelineId, - deletedOn: Date.now(), - }); - } - }); - this.setRecentlyDeleted(recentlyDeletedPipelines); - } - - removeFromRecentlyDeleted(...pipelineIds) { - const recentlyDeletedPipelinesToKeep = this.getRecentlyDeleted().filter( - recentlyDeletedPipeline => !pipelineIds.includes(recentlyDeletedPipeline.id) - ); - this.setRecentlyDeleted(recentlyDeletedPipelinesToKeep); - } - - isRecentlyDeleted(pipelineId) { - return this.getRecentlyDeleted() - .map(pipeline => pipeline.id) - .includes(pipelineId); - } - - getRecentlyDeleted() { - const recentlyDeletedPipelines = this.$window.localStorage.getItem( - RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY - ); - if (!recentlyDeletedPipelines) { - return []; - } - - return JSON.parse(recentlyDeletedPipelines); - } - - setRecentlyDeleted(recentlyDeletedPipelineIds) { - this.$window.localStorage.setItem( - RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY, - JSON.stringify(recentlyDeletedPipelineIds) - ); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/security/index.js b/x-pack/legacy/plugins/logstash/public/services/security/index.js deleted file mode 100755 index c9ff911723156..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/security/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { logstashSecurity } from './logstash_security'; diff --git a/x-pack/legacy/plugins/logstash/public/services/security/logstash_security.js b/x-pack/legacy/plugins/logstash/public/services/security/logstash_security.js deleted file mode 100755 index 0949038c9b6c7..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/security/logstash_security.js +++ /dev/null @@ -1,13 +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 { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -export const logstashSecurity = { - isSecurityEnabled() { - return Boolean(xpackInfo.get(`features.security`)); - }, -}; diff --git a/x-pack/legacy/plugins/logstash/public/services/upgrade/index.js b/x-pack/legacy/plugins/logstash/public/services/upgrade/index.js deleted file mode 100755 index 345d0d0ff68c6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/upgrade/index.js +++ /dev/null @@ -1,7 +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 './upgrade_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.factory.js deleted file mode 100755 index 925c6ae677bdf..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.factory.js +++ /dev/null @@ -1,13 +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 { uiModules } from 'ui/modules'; -import { UpgradeService } from './upgrade_service'; - -uiModules.get('xpack/logstash').factory('upgradeService', $injector => { - const $http = $injector.get('$http'); - return new UpgradeService($http); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js b/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js deleted file mode 100755 index 2019bdc1bf1aa..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js +++ /dev/null @@ -1,24 +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 chrome from 'ui/chrome'; -import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; - -export class UpgradeService { - constructor($http) { - this.$http = $http; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - executeUpgrade() { - return this.$http - .post(`${this.basePath}/upgrade`) - .then(response => response.data.is_upgraded) - .catch(e => { - throw e.data.message; - }); - } -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/check_license/__tests__/check_license.js b/x-pack/legacy/plugins/logstash/server/lib/check_license/__tests__/check_license.js deleted file mode 100755 index 5fcce0aaa1219..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/check_license/__tests__/check_license.js +++ /dev/null @@ -1,179 +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 expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicense } from '../check_license'; - -describe('check_license', function() { - let mockLicenseInfo; - beforeEach(() => (mockLicenseInfo = {})); - - describe('license information is undefined', () => { - beforeEach(() => (mockLicenseInfo = undefined)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', () => true); - mockLicenseInfo.feature = () => ({ isEnabled: () => true }); // Security feature is enabled - }); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should set isReadOnly to true', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(true); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', () => false); - mockLicenseInfo.feature = () => ({ isEnabled: () => true }); // Security feature is enabled - }); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& security is disabled', () => { - beforeEach(() => { - mockLicenseInfo.feature = () => ({ isEnabled: () => false }); // Security feature is disabled - set(mockLicenseInfo, 'license.isOneOf', () => true); - set(mockLicenseInfo, 'license.isActive', () => true); - }); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/check_license/check_license.js b/x-pack/legacy/plugins/logstash/server/lib/check_license/check_license.js deleted file mode 100755 index 31136ae1c72a5..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/check_license/check_license.js +++ /dev/null @@ -1,86 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable the Logstash pipeline UI - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - enableLinks: false, - isReadOnly: false, - message: i18n.translate( - 'xpack.logstash.managementSection.notPossibleToManagePipelinesMessage', - { - defaultMessage: - 'You cannot manage Logstash pipelines because license information is not available at this time.', - } - ), - }; - } - - const VALID_LICENSE_MODES = ['trial', 'standard', 'gold', 'platinum', 'enterprise']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - const isSecurityEnabled = xpackLicenseInfo.feature('security').isEnabled(); - - // Security is not enabled in ES - if (!isSecurityEnabled) { - const message = i18n.translate('xpack.logstash.managementSection.enableSecurityDescription', { - defaultMessage: - 'Security must be enabled in order to use Logstash pipeline management features.' + - ' Please set xpack.security.enabled: true in your elasticsearch.yml.', - }); - return { - isAvailable: false, - enableLinks: false, - isReadOnly: false, - message, - }; - } - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - enableLinks: false, - isReadOnly: false, - message: i18n.translate('xpack.logstash.managementSection.licenseDoesNotSupportDescription', { - defaultMessage: - 'Your {licenseType} license does not support Logstash pipeline management features. Please upgrade your license.', - values: { licenseType }, - }), - }; - } - - // License is valid but not active, we go into a read-only mode. - if (!isLicenseActive) { - return { - isAvailable: true, - enableLinks: true, - isReadOnly: true, - message: i18n.translate( - 'xpack.logstash.managementSection.pipelineCrudOperationsNotAllowedDescription', - { - defaultMessage: - 'You cannot edit, create, or delete your Logstash pipelines because your {licenseType} license has expired.', - values: { licenseType }, - } - ), - }; - } - - // License is valid and active - return { - isAvailable: true, - enableLinks: true, - isReadOnly: false, - }; -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/check_license/index.js b/x-pack/legacy/plugins/logstash/server/lib/check_license/index.js deleted file mode 100755 index f2c070fd44b6e..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/check_license/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { checkLicense } from './check_license'; diff --git a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/index.js b/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/index.js deleted file mode 100755 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js deleted file mode 100755 index a0d06e77b410d..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,21 +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 { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status'; -import { checkLicense } from '../check_license'; -import { PLUGIN } from '../../../../../../plugins/logstash/common/constants'; - -export function registerLicenseChecker(server) { - const xpackMainPlugin = server.plugins.xpack_main; - const logstashPlugin = server.plugins.logstash; - - mirrorPluginStatus(xpackMainPlugin, logstashPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/maps/common/constants.ts b/x-pack/legacy/plugins/maps/common/constants.ts deleted file mode 100644 index 98945653c25dc..0000000000000 --- a/x-pack/legacy/plugins/maps/common/constants.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/constants'; diff --git a/x-pack/legacy/plugins/maps/common/descriptor_types.ts b/x-pack/legacy/plugins/maps/common/descriptor_types.ts deleted file mode 100644 index 1f0eda26e7f7d..0000000000000 --- a/x-pack/legacy/plugins/maps/common/descriptor_types.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/descriptor_types'; diff --git a/x-pack/legacy/plugins/maps/common/get_join_key.ts b/x-pack/legacy/plugins/maps/common/get_join_key.ts deleted file mode 100644 index 004f12ca08d2e..0000000000000 --- a/x-pack/legacy/plugins/maps/common/get_join_key.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/get_join_key'; diff --git a/x-pack/legacy/plugins/maps/common/i18n_getters.ts b/x-pack/legacy/plugins/maps/common/i18n_getters.ts deleted file mode 100644 index f9d186dea2e2b..0000000000000 --- a/x-pack/legacy/plugins/maps/common/i18n_getters.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/i18n_getters'; diff --git a/x-pack/legacy/plugins/maps/common/parse_xml_string.js b/x-pack/legacy/plugins/maps/common/parse_xml_string.js deleted file mode 100644 index 34ec144472828..0000000000000 --- a/x-pack/legacy/plugins/maps/common/parse_xml_string.js +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/parse_xml_string'; diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index f4e01efc05f45..d1e8892fa2c98 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -9,9 +9,14 @@ import mappings from './mappings.json'; import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; import { migrations } from './migrations'; -import { getAppTitle } from './common/i18n_getters'; +import { getAppTitle } from '../../../plugins/maps/common/i18n_getters'; import { MapPlugin } from './server/plugin'; -import { APP_ID, APP_ICON, createMapPath, MAP_SAVED_OBJECT_TYPE } from './common/constants'; +import { + APP_ID, + APP_ICON, + createMapPath, + MAP_SAVED_OBJECT_TYPE, +} from '../../../plugins/maps/common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export function maps(kibana) { @@ -52,8 +57,6 @@ export function maps(kibana) { tilemap: _.get(mapConfig, 'tilemap', []), }; }, - embeddableFactories: ['plugins/maps/embeddable/map_embeddable_factory'], - home: ['plugins/maps/legacy_register_feature'], styleSheetPaths: `${__dirname}/public/index.scss`, savedObjectSchemas: { 'maps-telemetry': { @@ -78,7 +81,6 @@ export function maps(kibana) { }, mappings, migrations, - hacks: ['plugins/maps/register_vis_type_alias'], }, config(Joi) { return Joi.object({ diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index a8e69eef7a02f..d3666025082b7 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { extractReferences } from './common/migrations/references'; -import { emsRasterTileToEmsVectorTile } from './common/migrations/ems_raster_tile_to_ems_vector_tile'; -import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort'; -import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_global_query'; -import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; -import { migrateSymbolStyleDescriptor } from './common/migrations/migrate_symbol_style_descriptor'; -import { migrateUseTopHitsToScalingType } from './common/migrations/scaling_type'; -import { migrateJoinAggKey } from './common/migrations/join_agg_key'; +import { extractReferences } from '../../../plugins/maps/common/migrations/references'; +import { emsRasterTileToEmsVectorTile } from '../../../plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile'; +import { topHitsTimeToSort } from '../../../plugins/maps/common/migrations/top_hits_time_to_sort'; +import { moveApplyGlobalQueryToSources } from '../../../plugins/maps/common/migrations/move_apply_global_query'; +import { addFieldMetaOptions } from '../../../plugins/maps/common/migrations/add_field_meta_options'; +import { migrateSymbolStyleDescriptor } from '../../../plugins/maps/common/migrations/migrate_symbol_style_descriptor'; +import { migrateUseTopHitsToScalingType } from '../../../plugins/maps/common/migrations/scaling_type'; +import { migrateJoinAggKey } from '../../../plugins/maps/common/migrations/join_agg_key'; export const migrations = { map: { diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts deleted file mode 100644 index 34f8c30b51874..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts +++ /dev/null @@ -1,8 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -export * from '../../../../../plugins/maps/public/actions/map_actions'; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js deleted file mode 100644 index 7bfbf5761c5b8..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ /dev/null @@ -1,978 +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 _ from 'lodash'; -import turf from 'turf'; -import turfBooleanContains from '@turf/boolean-contains'; -import uuid from 'uuid/v4'; -import { - getLayerList, - getLayerListRaw, - getDataFilters, - getSelectedLayerId, - getMapReady, - getWaitingForMapReadyLayerListRaw, - getTransientLayerId, - getOpenTooltips, - getQuery, - getDataRequestDescriptor, -} from '../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../plugins/maps/public/reducers/ui'; -import { - cancelRequest, - registerCancelCallback, - unregisterCancelCallback, - getEventHandlers, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; -import { updateFlyout } from '../actions/ui_actions'; -import { - FEATURE_ID_PROPERTY_NAME, - LAYER_TYPE, - SOURCE_DATA_ID_ORIGIN, -} from '../../common/constants'; - -import { - SET_SELECTED_LAYER, - SET_TRANSIENT_LAYER, - UPDATE_LAYER_ORDER, - ADD_LAYER, - SET_LAYER_ERROR_STATUS, - ADD_WAITING_FOR_MAP_READY_LAYER, - CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, - REMOVE_LAYER, - SET_LAYER_VISIBILITY, - MAP_EXTENT_CHANGED, - MAP_READY, - MAP_DESTROYED, - LAYER_DATA_LOAD_STARTED, - LAYER_DATA_LOAD_ENDED, - LAYER_DATA_LOAD_ERROR, - UPDATE_SOURCE_DATA_REQUEST, - SET_JOINS, - SET_QUERY, - TRIGGER_REFRESH_TIMER, - UPDATE_LAYER_PROP, - UPDATE_LAYER_STYLE, - SET_LAYER_STYLE_META, - UPDATE_SOURCE_PROP, - SET_REFRESH_CONFIG, - SET_MOUSE_COORDINATES, - CLEAR_MOUSE_COORDINATES, - SET_GOTO, - CLEAR_GOTO, - TRACK_CURRENT_LAYER_STATE, - ROLLBACK_TO_TRACKED_LAYER_STATE, - REMOVE_TRACKED_LAYER_STATE, - SET_OPEN_TOOLTIPS, - UPDATE_DRAW_STATE, - SET_SCROLL_ZOOM, - SET_MAP_INIT_ERROR, - SET_INTERACTIVE, - DISABLE_TOOLTIP_CONTROL, - HIDE_TOOLBAR_OVERLAY, - HIDE_LAYER_CONTROL, - HIDE_VIEW_CONTROL, - SET_WAITING_FOR_READY_HIDDEN_LAYERS, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/actions/map_actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../../plugins/maps/public/actions/map_actions'; - -function getLayerLoadingCallbacks(dispatch, getState, layerId) { - return { - startLoading: (dataId, requestToken, meta) => - dispatch(startDataLoad(layerId, dataId, requestToken, meta)), - stopLoading: (dataId, requestToken, data, meta) => - dispatch(endDataLoad(layerId, dataId, requestToken, data, meta)), - onLoadError: (dataId, requestToken, errorMessage) => - dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)), - updateSourceData: newData => { - dispatch(updateSourceDataRequest(layerId, newData)); - }, - isRequestStillActive: (dataId, requestToken) => { - const dataRequest = getDataRequestDescriptor(getState(), layerId, dataId); - if (!dataRequest) { - return false; - } - return dataRequest.dataRequestToken === requestToken; - }, - registerCancelCallback: (requestToken, callback) => - dispatch(registerCancelCallback(requestToken, callback)), - }; -} - -function getLayerById(layerId, state) { - return getLayerList(state).find(layer => { - return layerId === layer.getId(); - }); -} - -async function syncDataForAllLayers(dispatch, getState, dataFilters) { - const state = getState(); - const layerList = getLayerList(state); - const syncs = layerList.map(layer => { - const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layer.getId()); - return layer.syncData({ ...loadingFunctions, dataFilters }); - }); - await Promise.all(syncs); -} - -export function cancelAllInFlightRequests() { - return (dispatch, getState) => { - getLayerList(getState()).forEach(layer => { - dispatch(clearDataRequests(layer)); - }); - }; -} - -function clearDataRequests(layer) { - return dispatch => { - layer.getInFlightRequestTokens().forEach(requestToken => { - dispatch(cancelRequest(requestToken)); - }); - dispatch({ - type: UPDATE_LAYER_PROP, - id: layer.getId(), - propName: '__dataRequests', - newValue: [], - }); - }; -} - -export function setMapInitError(errorMessage) { - return { - type: SET_MAP_INIT_ERROR, - errorMessage, - }; -} - -export function trackCurrentLayerState(layerId) { - return { - type: TRACK_CURRENT_LAYER_STATE, - layerId: layerId, - }; -} - -export function rollbackToTrackedLayerStateForSelectedLayer() { - return async (dispatch, getState) => { - const layerId = getSelectedLayerId(getState()); - await dispatch({ - type: ROLLBACK_TO_TRACKED_LAYER_STATE, - layerId: layerId, - }); - - // Ensure updateStyleMeta is triggered - // syncDataForLayer may not trigger endDataLoad if no re-fetch is required - dispatch(updateStyleMeta(layerId)); - - dispatch(syncDataForLayer(layerId)); - }; -} - -export function removeTrackedLayerStateForSelectedLayer() { - return (dispatch, getState) => { - const layerId = getSelectedLayerId(getState()); - dispatch({ - type: REMOVE_TRACKED_LAYER_STATE, - layerId: layerId, - }); - }; -} - -export function replaceLayerList(newLayerList) { - return (dispatch, getState) => { - const isMapReady = getMapReady(getState()); - if (!isMapReady) { - dispatch({ - type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, - }); - } else { - getLayerListRaw(getState()).forEach(({ id }) => { - dispatch(removeLayerFromLayerList(id)); - }); - } - - newLayerList.forEach(layerDescriptor => { - dispatch(addLayer(layerDescriptor)); - }); - }; -} - -export function cloneLayer(layerId) { - return async (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer) { - return; - } - - const clonedDescriptor = await layer.cloneDescriptor(); - dispatch(addLayer(clonedDescriptor)); - }; -} - -export function addLayer(layerDescriptor) { - return (dispatch, getState) => { - const isMapReady = getMapReady(getState()); - if (!isMapReady) { - dispatch({ - type: ADD_WAITING_FOR_MAP_READY_LAYER, - layer: layerDescriptor, - }); - return; - } - - dispatch({ - type: ADD_LAYER, - layer: layerDescriptor, - }); - dispatch(syncDataForLayer(layerDescriptor.id)); - }; -} - -// Do not use when rendering a map. Method exists to enable selectors for getLayerList when -// rendering is not needed. -export function addLayerWithoutDataSync(layerDescriptor) { - return { - type: ADD_LAYER, - layer: layerDescriptor, - }; -} - -function setLayerDataLoadErrorStatus(layerId, errorMessage) { - return dispatch => { - dispatch({ - type: SET_LAYER_ERROR_STATUS, - isInErrorState: errorMessage !== null, - layerId, - errorMessage, - }); - }; -} - -export function cleanTooltipStateForLayer(layerId, layerFeatures = []) { - return (dispatch, getState) => { - let featuresRemoved = false; - const openTooltips = getOpenTooltips(getState()) - .map(tooltipState => { - const nextFeatures = tooltipState.features.filter(tooltipFeature => { - if (tooltipFeature.layerId !== layerId) { - // feature from another layer, keep it - return true; - } - - // Keep feature if it is still in layer - return layerFeatures.some(layerFeature => { - return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; - }); - }); - - if (tooltipState.features.length !== nextFeatures.length) { - featuresRemoved = true; - } - - return { ...tooltipState, features: nextFeatures }; - }) - .filter(tooltipState => { - return tooltipState.features.length > 0; - }); - - if (featuresRemoved) { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips, - }); - } - }; -} - -export function setLayerVisibility(layerId, makeVisible) { - return async (dispatch, getState) => { - //if the current-state is invisible, we also want to sync data - //e.g. if a layer was invisible at start-up, it won't have any data loaded - const layer = getLayerById(layerId, getState()); - - // If the layer visibility is already what we want it to be, do nothing - if (!layer || layer.isVisible() === makeVisible) { - return; - } - - if (!makeVisible) { - dispatch(cleanTooltipStateForLayer(layerId)); - } - - await dispatch({ - type: SET_LAYER_VISIBILITY, - layerId, - visibility: makeVisible, - }); - if (makeVisible) { - dispatch(syncDataForLayer(layerId)); - } - }; -} - -export function toggleLayerVisible(layerId) { - return async (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer) { - return; - } - const makeVisible = !layer.isVisible(); - - dispatch(setLayerVisibility(layerId, makeVisible)); - }; -} - -export function setSelectedLayer(layerId) { - return async (dispatch, getState) => { - const oldSelectedLayer = getSelectedLayerId(getState()); - if (oldSelectedLayer) { - await dispatch(rollbackToTrackedLayerStateForSelectedLayer()); - } - if (layerId) { - dispatch(trackCurrentLayerState(layerId)); - } - dispatch({ - type: SET_SELECTED_LAYER, - selectedLayerId: layerId, - }); - }; -} - -export function removeTransientLayer() { - return async (dispatch, getState) => { - const transientLayerId = getTransientLayerId(getState()); - if (transientLayerId) { - await dispatch(removeLayerFromLayerList(transientLayerId)); - await dispatch(setTransientLayer(null)); - } - }; -} - -export function setTransientLayer(layerId) { - return { - type: SET_TRANSIENT_LAYER, - transientLayerId: layerId, - }; -} - -export function clearTransientLayerStateAndCloseFlyout() { - return async dispatch => { - await dispatch(updateFlyout(FLYOUT_STATE.NONE)); - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - }; -} - -export function updateLayerOrder(newLayerOrder) { - return { - type: UPDATE_LAYER_ORDER, - newLayerOrder, - }; -} - -export function mapReady() { - return (dispatch, getState) => { - dispatch({ - type: MAP_READY, - }); - - getWaitingForMapReadyLayerListRaw(getState()).forEach(layerDescriptor => { - dispatch(addLayer(layerDescriptor)); - }); - - dispatch({ - type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, - }); - }; -} - -export function mapDestroyed() { - return { - type: MAP_DESTROYED, - }; -} - -export function mapExtentChanged(newMapConstants) { - return async (dispatch, getState) => { - const state = getState(); - const dataFilters = getDataFilters(state); - const { extent, zoom: newZoom } = newMapConstants; - const { buffer, zoom: currentZoom } = dataFilters; - - if (extent) { - let doesBufferContainExtent = false; - if (buffer) { - const bufferGeometry = turf.bboxPolygon([ - buffer.minLon, - buffer.minLat, - buffer.maxLon, - buffer.maxLat, - ]); - const extentGeometry = turf.bboxPolygon([ - extent.minLon, - extent.minLat, - extent.maxLon, - extent.maxLat, - ]); - - doesBufferContainExtent = turfBooleanContains(bufferGeometry, extentGeometry); - } - - if (!doesBufferContainExtent || currentZoom !== newZoom) { - const scaleFactor = 0.5; // TODO put scale factor in store and fetch with selector - const width = extent.maxLon - extent.minLon; - const height = extent.maxLat - extent.minLat; - dataFilters.buffer = { - minLon: extent.minLon - width * scaleFactor, - minLat: extent.minLat - height * scaleFactor, - maxLon: extent.maxLon + width * scaleFactor, - maxLat: extent.maxLat + height * scaleFactor, - }; - } - } - - dispatch({ - type: MAP_EXTENT_CHANGED, - mapState: { - ...dataFilters, - ...newMapConstants, - }, - }); - const newDataFilters = { ...dataFilters, ...newMapConstants }; - await syncDataForAllLayers(dispatch, getState, newDataFilters); - }; -} - -export function closeOnClickTooltip(tooltipId) { - return (dispatch, getState) => { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips: getOpenTooltips(getState()).filter(({ id }) => { - return tooltipId !== id; - }), - }); - }; -} - -export function openOnClickTooltip(tooltipState) { - return (dispatch, getState) => { - const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => { - return ( - isLocked && - !_.isEqual(location, tooltipState.location) && - !_.isEqual(features, tooltipState.features) - ); - }); - - openTooltips.push({ - ...tooltipState, - isLocked: true, - id: uuid(), - }); - - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips, - }); - }; -} - -export function closeOnHoverTooltip() { - return (dispatch, getState) => { - if (getOpenTooltips(getState()).length) { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips: [], - }); - } - }; -} - -export function openOnHoverTooltip(tooltipState) { - return { - type: SET_OPEN_TOOLTIPS, - openTooltips: [ - { - ...tooltipState, - isLocked: false, - id: uuid(), - }, - ], - }; -} - -export function setMouseCoordinates({ lat, lon }) { - let safeLon = lon; - if (lon > 180) { - const overlapWestOfDateLine = lon - 180; - safeLon = -180 + overlapWestOfDateLine; - } else if (lon < -180) { - const overlapEastOfDateLine = Math.abs(lon) - 180; - safeLon = 180 - overlapEastOfDateLine; - } - - return { - type: SET_MOUSE_COORDINATES, - lat, - lon: safeLon, - }; -} - -export function clearMouseCoordinates() { - return { type: CLEAR_MOUSE_COORDINATES }; -} - -export function disableScrollZoom() { - return { type: SET_SCROLL_ZOOM, scrollZoom: false }; -} - -export function fitToLayerExtent(layerId) { - return async function(dispatch, getState) { - const targetLayer = getLayerById(layerId, getState()); - - if (targetLayer) { - const dataFilters = getDataFilters(getState()); - const bounds = await targetLayer.getBounds(dataFilters); - if (bounds) { - await dispatch(setGotoWithBounds(bounds)); - } - } - }; -} - -export function setGotoWithBounds(bounds) { - return { - type: SET_GOTO, - bounds: bounds, - }; -} - -export function setGotoWithCenter({ lat, lon, zoom }) { - return { - type: SET_GOTO, - center: { lat, lon, zoom }, - }; -} - -export function clearGoto() { - return { type: CLEAR_GOTO }; -} - -export function startDataLoad(layerId, dataId, requestToken, meta = {}) { - return (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (layer) { - dispatch(cancelRequest(layer.getPrevRequestToken(dataId))); - } - - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoad) { - eventHandlers.onDataLoad({ - layerId, - dataId, - }); - } - - dispatch({ - meta, - type: LAYER_DATA_LOAD_STARTED, - layerId, - dataId, - requestToken, - }); - }; -} - -export function updateSourceDataRequest(layerId, newData) { - return dispatch => { - dispatch({ - type: UPDATE_SOURCE_DATA_REQUEST, - dataId: SOURCE_DATA_ID_ORIGIN, - layerId, - newData, - }); - - dispatch(updateStyleMeta(layerId)); - }; -} - -export function endDataLoad(layerId, dataId, requestToken, data, meta) { - return async (dispatch, getState) => { - dispatch(unregisterCancelCallback(requestToken)); - - const features = data && data.features ? data.features : []; - - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadEnd) { - const layer = getLayerById(layerId, getState()); - const resultMeta = {}; - if (layer && layer.getType() === LAYER_TYPE.VECTOR) { - resultMeta.featuresCount = features.length; - } - - eventHandlers.onDataLoadEnd({ - layerId, - dataId, - resultMeta, - }); - } - - dispatch(cleanTooltipStateForLayer(layerId, features)); - dispatch({ - type: LAYER_DATA_LOAD_ENDED, - layerId, - dataId, - data, - meta, - requestToken, - }); - - //Clear any data-load errors when there is a succesful data return. - //Co this on end-data-load iso at start-data-load to avoid blipping the error status between true/false. - //This avoids jitter in the warning icon of the TOC when the requests continues to return errors. - dispatch(setLayerDataLoadErrorStatus(layerId, null)); - - dispatch(updateStyleMeta(layerId)); - }; -} - -export function onDataLoadError(layerId, dataId, requestToken, errorMessage) { - return async (dispatch, getState) => { - dispatch(unregisterCancelCallback(requestToken)); - - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadError) { - eventHandlers.onDataLoadError({ - layerId, - dataId, - errorMessage, - }); - } - - dispatch(cleanTooltipStateForLayer(layerId)); - dispatch({ - type: LAYER_DATA_LOAD_ERROR, - data: null, - layerId, - dataId, - requestToken, - }); - - dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); - }; -} - -export function updateSourceProp(layerId, propName, value, newLayerType) { - return async dispatch => { - dispatch({ - type: UPDATE_SOURCE_PROP, - layerId, - propName, - value, - }); - if (newLayerType) { - dispatch(updateLayerType(layerId, newLayerType)); - } - await dispatch(clearMissingStyleProperties(layerId)); - dispatch(syncDataForLayer(layerId)); - }; -} - -function updateLayerType(layerId, newLayerType) { - return (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer || layer.getType() === newLayerType) { - return; - } - dispatch(clearDataRequests(layer)); - dispatch({ - type: UPDATE_LAYER_PROP, - id: layerId, - propName: 'type', - newValue: newLayerType, - }); - }; -} - -export function syncDataForLayer(layerId) { - return async (dispatch, getState) => { - const targetLayer = getLayerById(layerId, getState()); - if (targetLayer) { - const dataFilters = getDataFilters(getState()); - const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layerId); - await targetLayer.syncData({ - ...loadingFunctions, - dataFilters, - }); - } - }; -} - -export function updateLayerLabel(id, newLabel) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'label', - newValue: newLabel, - }; -} - -export function updateLayerMinZoom(id, minZoom) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'minZoom', - newValue: minZoom, - }; -} - -export function updateLayerMaxZoom(id, maxZoom) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'maxZoom', - newValue: maxZoom, - }; -} - -export function updateLayerAlpha(id, alpha) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'alpha', - newValue: alpha, - }; -} - -export function setLayerQuery(id, query) { - return dispatch => { - dispatch({ - type: UPDATE_LAYER_PROP, - id, - propName: 'query', - newValue: query, - }); - - dispatch(syncDataForLayer(id)); - }; -} - -export function removeSelectedLayer() { - return (dispatch, getState) => { - const state = getState(); - const layerId = getSelectedLayerId(state); - dispatch(removeLayer(layerId)); - }; -} - -export function removeLayer(layerId) { - return async (dispatch, getState) => { - const state = getState(); - const selectedLayerId = getSelectedLayerId(state); - if (layerId === selectedLayerId) { - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - await dispatch(setSelectedLayer(null)); - } - dispatch(removeLayerFromLayerList(layerId)); - }; -} - -function removeLayerFromLayerList(layerId) { - return (dispatch, getState) => { - const layerGettingRemoved = getLayerById(layerId, getState()); - if (!layerGettingRemoved) { - return; - } - - layerGettingRemoved.getInFlightRequestTokens().forEach(requestToken => { - dispatch(cancelRequest(requestToken)); - }); - dispatch(cleanTooltipStateForLayer(layerId)); - layerGettingRemoved.destroy(); - dispatch({ - type: REMOVE_LAYER, - id: layerId, - }); - }; -} - -export function setQuery({ query, timeFilters, filters = [], refresh = false }) { - function generateQueryTimestamp() { - return new Date().toISOString(); - } - return async (dispatch, getState) => { - const prevQuery = getQuery(getState()); - const prevTriggeredAt = - prevQuery && prevQuery.queryLastTriggeredAt - ? prevQuery.queryLastTriggeredAt - : generateQueryTimestamp(); - - dispatch({ - type: SET_QUERY, - timeFilters, - query: { - ...query, - // ensure query changes to trigger re-fetch when "Refresh" clicked - queryLastTriggeredAt: refresh ? generateQueryTimestamp() : prevTriggeredAt, - }, - filters, - }); - - const dataFilters = getDataFilters(getState()); - await syncDataForAllLayers(dispatch, getState, dataFilters); - }; -} - -export function setRefreshConfig({ isPaused, interval }) { - return { - type: SET_REFRESH_CONFIG, - isPaused, - interval, - }; -} - -export function triggerRefreshTimer() { - return async (dispatch, getState) => { - dispatch({ - type: TRIGGER_REFRESH_TIMER, - }); - - const dataFilters = getDataFilters(getState()); - await syncDataForAllLayers(dispatch, getState, dataFilters); - }; -} - -export function clearMissingStyleProperties(layerId) { - return async (dispatch, getState) => { - const targetLayer = getLayerById(layerId, getState()); - if (!targetLayer) { - return; - } - - const style = targetLayer.getCurrentStyle(); - if (!style) { - return; - } - - const nextFields = await targetLayer.getFields(); //take into account all fields, since labels can be driven by any field (source or join) - const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved( - nextFields - ); - if (hasChanges) { - dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); - } - }; -} - -export function updateLayerStyle(layerId, styleDescriptor) { - return dispatch => { - dispatch({ - type: UPDATE_LAYER_STYLE, - layerId, - style: { - ...styleDescriptor, - }, - }); - - // Ensure updateStyleMeta is triggered - // syncDataForLayer may not trigger endDataLoad if no re-fetch is required - dispatch(updateStyleMeta(layerId)); - - // Style update may require re-fetch, for example ES search may need to retrieve field used for dynamic styling - dispatch(syncDataForLayer(layerId)); - }; -} - -export function updateStyleMeta(layerId) { - return async (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer) { - return; - } - const sourceDataRequest = layer.getSourceDataRequest(); - const style = layer.getCurrentStyle(); - if (!style || !sourceDataRequest) { - return; - } - const styleMeta = await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); - dispatch({ - type: SET_LAYER_STYLE_META, - layerId, - styleMeta, - }); - }; -} - -export function updateLayerStyleForSelectedLayer(styleDescriptor) { - return (dispatch, getState) => { - const selectedLayerId = getSelectedLayerId(getState()); - if (!selectedLayerId) { - return; - } - dispatch(updateLayerStyle(selectedLayerId, styleDescriptor)); - }; -} - -export function setJoinsForLayer(layer, joins) { - return async dispatch => { - await dispatch({ - type: SET_JOINS, - layer: layer, - joins: joins, - }); - - await dispatch(clearMissingStyleProperties(layer.getId())); - dispatch(syncDataForLayer(layer.getId())); - }; -} - -export function updateDrawState(drawState) { - return dispatch => { - if (drawState !== null) { - dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way - } - dispatch({ - type: UPDATE_DRAW_STATE, - drawState: drawState, - }); - }; -} - -export function disableInteractive() { - return { type: SET_INTERACTIVE, disableInteractive: true }; -} - -export function disableTooltipControl() { - return { type: DISABLE_TOOLTIP_CONTROL, disableTooltipControl: true }; -} - -export function hideToolbarOverlay() { - return { type: HIDE_TOOLBAR_OVERLAY, hideToolbarOverlay: true }; -} - -export function hideLayerControl() { - return { type: HIDE_LAYER_CONTROL, hideLayerControl: true }; -} -export function hideViewControl() { - return { type: HIDE_VIEW_CONTROL, hideViewControl: true }; -} - -export function setHiddenLayers(hiddenLayerIds) { - return (dispatch, getState) => { - const isMapReady = getMapReady(getState()); - - if (!isMapReady) { - dispatch({ type: SET_WAITING_FOR_READY_HIDDEN_LAYERS, hiddenLayerIds }); - } else { - getLayerListRaw(getState()).forEach(layer => - dispatch(setLayerVisibility(layer.id, !hiddenLayerIds.includes(layer.id))) - ); - } - }; -} diff --git a/x-pack/legacy/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/ui_actions.d.ts deleted file mode 100644 index 233918847de08..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/ui_actions.d.ts +++ /dev/null @@ -1,13 +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 { AnyAction } from 'redux'; - -export function setOpenTOCDetails(layerIds?: string[]): AnyAction; - -export function setIsLayerTOCOpen(open: boolean): AnyAction; - -export function setReadOnly(readOnly: boolean): AnyAction; diff --git a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js b/x-pack/legacy/plugins/maps/public/actions/ui_actions.js deleted file mode 100644 index 33ab2fd74122a..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js +++ /dev/null @@ -1,91 +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 { - UPDATE_FLYOUT, - CLOSE_SET_VIEW, - OPEN_SET_VIEW, - SET_IS_LAYER_TOC_OPEN, - SET_FULL_SCREEN, - SET_READ_ONLY, - SET_OPEN_TOC_DETAILS, - SHOW_TOC_DETAILS, - HIDE_TOC_DETAILS, - UPDATE_INDEXING_STAGE, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/actions/ui_actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../../plugins/maps/public/actions/ui_actions'; - -export function exitFullScreen() { - return { - type: SET_FULL_SCREEN, - isFullScreen: false, - }; -} - -export function updateFlyout(display) { - return { - type: UPDATE_FLYOUT, - display, - }; -} -export function closeSetView() { - return { - type: CLOSE_SET_VIEW, - }; -} -export function openSetView() { - return { - type: OPEN_SET_VIEW, - }; -} -export function setIsLayerTOCOpen(isLayerTOCOpen) { - return { - type: SET_IS_LAYER_TOC_OPEN, - isLayerTOCOpen, - }; -} -export function enableFullScreen() { - return { - type: SET_FULL_SCREEN, - isFullScreen: true, - }; -} -export function setReadOnly(isReadOnly) { - return { - type: SET_READ_ONLY, - isReadOnly, - }; -} - -export function setOpenTOCDetails(layerIds) { - return { - type: SET_OPEN_TOC_DETAILS, - layerIds, - }; -} - -export function showTOCDetails(layerId) { - return { - type: SHOW_TOC_DETAILS, - layerId, - }; -} - -export function hideTOCDetails(layerId) { - return { - type: HIDE_TOC_DETAILS, - layerId, - }; -} - -export function updateIndexingStage(stage) { - return { - type: UPDATE_INDEXING_STAGE, - stage, - }; -} diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js deleted file mode 100644 index 686259aeaaba4..0000000000000 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js +++ /dev/null @@ -1,54 +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 _ from 'lodash'; -// Import each layer type, even those not used, to init in registry -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/wms_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/ems_file_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/es_search_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/kibana_regionmap_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/es_geo_grid_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/xyz_tms_source'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { KibanaTilemapSource } from '../../../../../plugins/maps/public/layers/sources/kibana_tilemap_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { EMSTMSSource } from '../../../../../plugins/maps/public/layers/sources/ems_tms_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInjectedVarFunc } from '../../../../../plugins/maps/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getKibanaTileMap } from '../../../../../plugins/maps/public/meta'; - -export function getInitialLayers(layerListJSON, initialLayers = []) { - if (layerListJSON) { - return JSON.parse(layerListJSON); - } - - const tilemapSourceFromKibana = getKibanaTileMap(); - if (_.get(tilemapSourceFromKibana, 'url')) { - const sourceDescriptor = KibanaTilemapSource.createDescriptor(); - const source = new KibanaTilemapSource(sourceDescriptor); - const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor(), ...initialLayers]; - } - - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); - if (isEmsEnabled) { - const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); - const source = new EMSTMSSource(descriptor); - const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor(), ...initialLayers]; - } - - return initialLayers; -} diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 6bc8a4d0be5ac..1b1fbf111fe04 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -28,8 +28,10 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; import { Provider } from 'react-redux'; -import { GisMap } from '../connected_components/gis_map'; -import { addHelpMenuToAppChrome } from '../help_menu_util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { GisMap } from '../../../../../plugins/maps/public/connected_components/gis_map'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { addHelpMenuToAppChrome } from '../../../../../plugins/maps/public/help_menu_util'; import { setSelectedLayer, setRefreshConfig, @@ -37,7 +39,9 @@ import { replaceLayerList, setQuery, clearTransientLayerStateAndCloseFlyout, -} from '../actions/map_actions'; + setMapSettings, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/actions/map_actions'; import { DEFAULT_IS_LAYER_TOC_OPEN, FLYOUT_STATE, @@ -49,22 +53,33 @@ import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails, -} from '../actions/ui_actions'; -import { getIsFullScreen } from '../selectors/ui_selectors'; + openMapSettings, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/actions/ui_actions'; +import { + getIsFullScreen, + getFlyoutDisplay, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/selectors/ui_selectors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { copyPersistentState } from '../../../../../plugins/maps/public/reducers/util'; import { getQueryableUniqueIndexPatternIds, hasDirtyState, getLayerListRaw, -} from '../selectors/map_selectors'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/selectors/map_selectors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; -import { getInitialLayers } from './get_initial_layers'; -import { getInitialQuery } from './get_initial_query'; -import { getInitialTimeFilters } from './get_initial_time_filters'; -import { getInitialRefreshConfig } from './get_initial_refresh_config'; -import { MAP_SAVED_OBJECT_TYPE, MAP_APP_PATH } from '../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialLayers } from '../../../../../plugins/maps/public/angular/get_initial_layers'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialQuery } from '../../../../../plugins/maps/public/angular/get_initial_query'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialTimeFilters } from '../../../../../plugins/maps/public/angular/get_initial_time_filters'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialRefreshConfig } from '../../../../../plugins/maps/public/angular/get_initial_refresh_config'; +import { MAP_SAVED_OBJECT_TYPE, MAP_APP_PATH } from '../../../../../plugins/maps/common/constants'; import { npSetup, npStart } from 'ui/new_platform'; import { esFilters } from '../../../../../../src/plugins/data/public'; import { @@ -385,6 +400,9 @@ app.controller( if (mapState.filters) { savedObjectFilters = mapState.filters; } + if (mapState.settings) { + store.dispatch(setMapSettings(mapState.settings)); + } } if (savedMap.uiStateJSON) { @@ -443,6 +461,7 @@ app.controller( $scope.isFullScreen = false; $scope.isSaveDisabled = false; + $scope.isOpenSettingsDisabled = false; function handleStoreChanges(store) { const nextIsFullScreen = getIsFullScreen(store.getState()); if (nextIsFullScreen !== $scope.isFullScreen) { @@ -464,6 +483,14 @@ app.controller( $scope.isSaveDisabled = nextIsSaveDisabled; }); } + + const flyoutDisplay = getFlyoutDisplay(store.getState()); + const nextIsOpenSettingsDisabled = flyoutDisplay !== FLYOUT_STATE.NONE; + if (nextIsOpenSettingsDisabled !== $scope.isOpenSettingsDisabled) { + $scope.$evalAsync(() => { + $scope.isOpenSettingsDisabled = nextIsOpenSettingsDisabled; + }); + } } $scope.$on('$destroy', () => { @@ -581,6 +608,22 @@ app.controller( getInspector().open(inspectorAdapters, {}); }, }, + { + id: 'mapSettings', + label: i18n.translate('xpack.maps.mapController.openSettingsButtonLabel', { + defaultMessage: `Map settings`, + }), + description: i18n.translate('xpack.maps.mapController.openSettingsDescription', { + defaultMessage: `Open map settings`, + }), + testId: 'openSettingsButton', + disableButton() { + return $scope.isOpenSettingsDisabled; + }, + run() { + store.dispatch(openMapSettings()); + }, + }, ...(getMapsCapabilities().save ? [ { diff --git a/x-pack/legacy/plugins/maps/public/components/_index.scss b/x-pack/legacy/plugins/maps/public/components/_index.scss deleted file mode 100644 index 0b32719442424..0000000000000 --- a/x-pack/legacy/plugins/maps/public/components/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './metric_editors'; -@import './geometry_filter'; -@import './tooltip_selector'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/_index.scss deleted file mode 100644 index 99a2e222ea6c1..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/_index.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import './gis_map/gis_map'; -@import './layer_addpanel/source_select/index'; -@import './layer_panel/index'; -@import './widget_overlay/index'; -@import './toolbar_overlay/index'; -@import './map/features_tooltip/index'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.d.ts b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.d.ts deleted file mode 100644 index 8689d88297171..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.d.ts +++ /dev/null @@ -1,15 +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 React from 'react'; -import { Filter } from 'src/plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderToolTipContent } from '../../../../../../plugins/maps/public/layers/tooltips/tooltip_property'; - -export const GisMap: React.ComponentType<{ - addFilters: ((filters: Filter[]) => void) | null; - renderTooltipContent?: RenderToolTipContent; -}>; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js deleted file mode 100644 index 2d8265bae9387..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js +++ /dev/null @@ -1,51 +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 { connect } from 'react-redux'; -import { GisMap } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../plugins/maps/public/reducers/ui'; -import { exitFullScreen } from '../../actions/ui_actions'; -import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; -import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions'; -import { - areLayersLoaded, - getRefreshConfig, - getMapInitError, - getQueryableUniqueIndexPatternIds, - isToolbarOverlayHidden, -} from '../../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getCoreChrome } from '../../../../../../plugins/maps/public/kibana_services'; - -function mapStateToProps(state = {}) { - const flyoutDisplay = getFlyoutDisplay(state); - return { - areLayersLoaded: areLayersLoaded(state), - layerDetailsVisible: flyoutDisplay === FLYOUT_STATE.LAYER_PANEL, - addLayerVisible: flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD, - noFlyoutVisible: flyoutDisplay === FLYOUT_STATE.NONE, - isFullScreen: getIsFullScreen(state), - refreshConfig: getRefreshConfig(state), - mapInitError: getMapInitError(state), - indexPatternIds: getQueryableUniqueIndexPatternIds(state), - hideToolbarOverlay: isToolbarOverlayHidden(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), - exitFullScreen: () => { - dispatch(exitFullScreen()); - getCoreChrome().setIsVisible(true); - }, - cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), - }; -} - -const connectedGisMap = connect(mapStateToProps, mapDispatchToProps)(GisMap); -export { connectedGisMap as GisMap }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js deleted file mode 100644 index 06097ebea1900..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js +++ /dev/null @@ -1,222 +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 _ from 'lodash'; -import React, { Component } from 'react'; -import { MBMapContainer } from '../map/mb'; -import { WidgetOverlay } from '../widget_overlay/index'; -import { ToolbarOverlay } from '../toolbar_overlay/index'; -import { LayerPanel } from '../layer_panel/index'; -import { AddLayerPanel } from '../layer_addpanel/index'; -import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; -import { ExitFullScreenButton } from '../../../../../../../src/plugins/kibana_react/public'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getIndexPatternsFromIds } from '../../../../../../plugins/maps/public/index_pattern_util'; -import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; -import { indexPatterns as indexPatternsUtils } from '../../../../../../../src/plugins/data/public'; -import { i18n } from '@kbn/i18n'; -import uuid from 'uuid/v4'; - -const RENDER_COMPLETE_EVENT = 'renderComplete'; - -export class GisMap extends Component { - state = { - isInitialLoadRenderTimeoutComplete: false, - domId: uuid(), - geoFields: [], - }; - - componentDidMount() { - this._isMounted = true; - this._isInitalLoadRenderTimerStarted = false; - this._setRefreshTimer(); - } - - componentDidUpdate() { - this._setRefreshTimer(); - if (this.props.areLayersLoaded && !this._isInitalLoadRenderTimerStarted) { - this._isInitalLoadRenderTimerStarted = true; - this._startInitialLoadRenderTimer(); - } - - if (!!this.props.addFilters) { - this._loadGeoFields(this.props.indexPatternIds); - } - } - - componentWillUnmount() { - this._isMounted = false; - this._clearRefreshTimer(); - this.props.cancelAllInFlightRequests(); - } - - // Reporting uses both a `data-render-complete` attribute and a DOM event listener to determine - // if a visualization is done loading. The process roughly is: - // - See if the `data-render-complete` attribute is "true". If so we're done! - // - If it's not, then reporting injects a listener into the browser for a custom "renderComplete" event. - // - When that event is fired, we snapshot the viz and move on. - // Failure to not have the dom attribute, or custom event, will timeout the job. - // See x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts for more. - _onInitialLoadRenderComplete = () => { - const el = document.querySelector(`[data-dom-id="${this.state.domId}"]`); - - if (el) { - el.dispatchEvent(new CustomEvent(RENDER_COMPLETE_EVENT, { bubbles: true })); - } - }; - - _loadGeoFields = async nextIndexPatternIds => { - if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { - // all ready loaded index pattern ids - return; - } - - this._prevIndexPatternIds = nextIndexPatternIds; - - const geoFields = []; - try { - const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); - indexPatterns.forEach(indexPattern => { - indexPattern.fields.forEach(field => { - if ( - !indexPatternsUtils.isNestedField(field) && - (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || - field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ) { - geoFields.push({ - geoFieldName: field.name, - geoFieldType: field.type, - indexPatternTitle: indexPattern.title, - indexPatternId: indexPattern.id, - }); - } - }); - }); - } catch (e) { - // swallow errors. - // the Layer-TOC will indicate which layers are disfunctional on a per-layer basis - } - - if (!this._isMounted) { - return; - } - - this.setState({ geoFields }); - }; - - _setRefreshTimer = () => { - const { isPaused, interval } = this.props.refreshConfig; - - if (this.isPaused === isPaused && this.interval === interval) { - // refreshConfig is the same, nothing to do - return; - } - - this.isPaused = isPaused; - this.interval = interval; - - this._clearRefreshTimer(); - - if (!isPaused && interval > 0) { - this.refreshTimerId = setInterval(() => { - this.props.triggerRefreshTimer(); - }, interval); - } - }; - - _clearRefreshTimer = () => { - if (this.refreshTimerId) { - clearInterval(this.refreshTimerId); - } - }; - - // Mapbox does not provide any feedback when rendering is complete. - // Temporary solution is just to wait set period of time after data has loaded. - _startInitialLoadRenderTimer = () => { - setTimeout(() => { - if (this._isMounted) { - this.setState({ isInitialLoadRenderTimeoutComplete: true }); - this._onInitialLoadRenderComplete(); - } - }, 5000); - }; - - render() { - const { - addFilters, - layerDetailsVisible, - addLayerVisible, - noFlyoutVisible, - isFullScreen, - exitFullScreen, - mapInitError, - renderTooltipContent, - } = this.props; - - const { domId } = this.state; - - if (mapInitError) { - return ( -
- -

{mapInitError}

-
-
- ); - } - - let currentPanel; - let currentPanelClassName; - if (noFlyoutVisible) { - currentPanel = null; - } else if (addLayerVisible) { - currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = ; - } else if (layerDetailsVisible) { - currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = ; - } - - let exitFullScreenButton; - if (isFullScreen) { - exitFullScreenButton = ; - } - return ( - - - - {!this.props.hideToolbarOverlay && ( - - )} - - - - - {currentPanel} - - - {exitFullScreenButton} - - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js deleted file mode 100644 index e8192795f98ae..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js +++ /dev/null @@ -1,31 +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 { connect } from 'react-redux'; -import { ImportEditor } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { INDEXING_STAGE } from '../../../../../../../plugins/maps/public/reducers/ui'; -import { updateIndexingStage } from '../../../actions/ui_actions'; -import { getIndexingStage } from '../../../selectors/ui_selectors'; - -function mapStateToProps(state = {}) { - return { - inspectorAdapters: getInspectorAdapters(state), - isIndexingTriggered: getIndexingStage(state) === INDEXING_STAGE.TRIGGERED, - }; -} - -const mapDispatchToProps = { - onIndexReady: indexReady => - indexReady ? updateIndexingStage(INDEXING_STAGE.READY) : updateIndexingStage(null), - importSuccessHandler: () => updateIndexingStage(INDEXING_STAGE.SUCCESS), - importErrorHandler: () => updateIndexingStage(INDEXING_STAGE.ERROR), -}; - -const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(ImportEditor); -export { connectedFlyOut as ImportEditor }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js deleted file mode 100644 index cb20d80733c33..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js +++ /dev/null @@ -1,54 +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 React, { Fragment } from 'react'; -import { EuiSpacer, EuiPanel, EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { uploadLayerWizardConfig } from '../../../../../../../plugins/maps/public/layers/sources/client_file_source'; - -export const ImportEditor = ({ clearSource, isIndexingTriggered, ...props }) => { - const editorProperties = getEditorProperties({ isIndexingTriggered, ...props }); - return ( - - {isIndexingTriggered ? null : ( - - - - - - - )} - - {uploadLayerWizardConfig.renderWizard(editorProperties)} - - - ); -}; - -function getEditorProperties({ - inspectorAdapters, - onRemove, - viewLayer, - isIndexingTriggered, - onIndexReady, - importSuccessHandler, - importErrorHandler, -}) { - return { - onPreviewSource: viewLayer, - inspectorAdapters, - onRemove, - importSuccessHandler, - importErrorHandler, - isIndexingTriggered, - addAndViewSource: viewLayer, - onIndexReady, - }; -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js deleted file mode 100644 index c4e2fa5169b0f..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js +++ /dev/null @@ -1,60 +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 { connect } from 'react-redux'; -import { AddLayerPanel } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE, INDEXING_STAGE } from '../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout, updateIndexingStage } from '../../actions/ui_actions'; -import { getFlyoutDisplay, getIndexingStage } from '../../selectors/ui_selectors'; -import { getMapColors } from '../../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../plugins/maps/public/reducers/non_serializable_instances'; -import { - setTransientLayer, - addLayer, - setSelectedLayer, - removeTransientLayer, -} from '../../actions/map_actions'; - -function mapStateToProps(state = {}) { - const indexingStage = getIndexingStage(state); - return { - inspectorAdapters: getInspectorAdapters(state), - flyoutVisible: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, - mapColors: getMapColors(state), - isIndexingTriggered: indexingStage === INDEXING_STAGE.TRIGGERED, - isIndexingSuccess: indexingStage === INDEXING_STAGE.SUCCESS, - isIndexingReady: indexingStage === INDEXING_STAGE.READY, - }; -} - -function mapDispatchToProps(dispatch) { - return { - viewLayer: async layer => { - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - dispatch(addLayer(layer.toLayerDescriptor())); - dispatch(setSelectedLayer(layer.getId())); - dispatch(setTransientLayer(layer.getId())); - }, - removeTransientLayer: () => { - dispatch(setSelectedLayer(null)); - dispatch(removeTransientLayer()); - }, - selectLayerAndAdd: () => { - dispatch(setTransientLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); - }, - setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)), - resetIndexing: () => dispatch(updateIndexingStage(null)), - }; -} - -const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })( - AddLayerPanel -); -export { connectedFlyOut as AddLayerPanel }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js deleted file mode 100644 index 553e54ee89766..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js +++ /dev/null @@ -1,19 +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 { connect } from 'react-redux'; -import { SourceEditor } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; - -function mapStateToProps(state = {}) { - return { - inspectorAdapters: getInspectorAdapters(state), - }; -} - -const connectedFlyOut = connect(mapStateToProps)(SourceEditor); -export { connectedFlyOut as SourceEditor }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss deleted file mode 100644 index 7fe1396fcca16..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './source_select'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss deleted file mode 100644 index fd074edf032fa..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './layer_panel'; -@import './filter_editor/filter_editor'; -@import './join_editor/resources/join'; -@import './style_settings/style_settings'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js deleted file mode 100644 index 287f0019f18ec..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js +++ /dev/null @@ -1,43 +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 { connect } from 'react-redux'; -import { FlyoutFooter } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout } from '../../../actions/ui_actions'; -import { hasDirtyState } from '../../../selectors/map_selectors'; -import { - setSelectedLayer, - removeSelectedLayer, - removeTrackedLayerStateForSelectedLayer, -} from '../../../actions/map_actions'; - -function mapStateToProps(state = {}) { - return { - hasStateChanged: hasDirtyState(state), - }; -} - -const mapDispatchToProps = dispatch => { - return { - cancelLayerPanel: () => { - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(setSelectedLayer(null)); - }, - saveLayerEdits: () => { - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removeTrackedLayerStateForSelectedLayer()); - dispatch(setSelectedLayer(null)); - }, - removeLayer: () => { - dispatch(removeSelectedLayer()); - }, - }; -}; - -const connectedFlyoutFooter = connect(mapStateToProps, mapDispatchToProps)(FlyoutFooter); -export { connectedFlyoutFooter as FlyoutFooter }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js deleted file mode 100644 index e8f980bbbf2b4..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js +++ /dev/null @@ -1,41 +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 { connect } from 'react-redux'; -import { LayerSettings } from './layer_settings'; -import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { - updateLayerLabel, - updateLayerMaxZoom, - updateLayerMinZoom, - updateLayerAlpha, -} from '../../../actions/map_actions'; -import { MAX_ZOOM } from '../../../../../../../plugins/maps/common/constants'; - -function mapStateToProps(state = {}) { - const selectedLayer = getSelectedLayer(state); - return { - minVisibilityZoom: selectedLayer.getMinSourceZoom(), - maxVisibilityZoom: MAX_ZOOM, - alpha: selectedLayer.getAlpha(), - label: selectedLayer.getLabel(), - layerId: selectedLayer.getId(), - maxZoom: selectedLayer.getMaxZoom(), - minZoom: selectedLayer.getMinZoom(), - }; -} - -function mapDispatchToProps(dispatch) { - return { - updateLabel: (id, label) => dispatch(updateLayerLabel(id, label)), - updateMinZoom: (id, minZoom) => dispatch(updateLayerMinZoom(id, minZoom)), - updateMaxZoom: (id, maxZoom) => dispatch(updateLayerMaxZoom(id, maxZoom)), - updateAlpha: (id, alpha) => dispatch(updateLayerAlpha(id, alpha)), - }; -} - -const connectedLayerSettings = connect(mapStateToProps, mapDispatchToProps)(LayerSettings); -export { connectedLayerSettings as LayerSettings }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js deleted file mode 100644 index 1d352913e54a3..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ /dev/null @@ -1,117 +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 React, { Fragment } from 'react'; - -import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ValidatedRange } from '../../../../../../../plugins/maps/public/components/validated_range'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ValidatedDualRange } from '../../../../../../../../src/plugins/kibana_react/public'; -export function LayerSettings(props) { - const onLabelChange = event => { - const label = event.target.value; - props.updateLabel(props.layerId, label); - }; - - const onZoomChange = ([min, max]) => { - props.updateMinZoom(props.layerId, Math.max(props.minVisibilityZoom, parseInt(min, 10))); - props.updateMaxZoom(props.layerId, Math.min(props.maxVisibilityZoom, parseInt(max, 10))); - }; - - const onAlphaChange = alpha => { - const alphaDecimal = alpha / 100; - props.updateAlpha(props.layerId, alphaDecimal); - }; - - const renderZoomSliders = () => { - return ( - - ); - }; - - const renderLabel = () => { - return ( - - - - ); - }; - - const renderAlphaSlider = () => { - const alphaPercent = Math.round(props.alpha * 100); - - return ( - - - - ); - }; - - return ( - - - -
- -
-
- - - {renderLabel()} - {renderZoomSliders()} - {renderAlphaSlider()} -
- - -
- ); -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.d.ts b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.d.ts deleted file mode 100644 index cf4fdc7be70c6..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.d.ts +++ /dev/null @@ -1,8 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -export * from '../../../../../../plugins/maps/public/connected_components/layer_panel/view'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js deleted file mode 100644 index 2521318f0b3c9..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ /dev/null @@ -1,238 +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 React, { Fragment } from 'react'; - -import { FilterEditor } from './filter_editor'; -import { JoinEditor } from './join_editor'; -import { FlyoutFooter } from './flyout_footer'; -import { LayerErrors } from './layer_errors'; -import { LayerSettings } from './layer_settings'; -import { StyleSettings } from './style_settings'; -import { - EuiButtonIcon, - EuiFlexItem, - EuiTitle, - EuiPanel, - EuiFlexGroup, - EuiFlyoutHeader, - EuiFlyoutFooter, - EuiSpacer, - EuiAccordion, - EuiText, - EuiLink, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; -import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getData, getCore } from '../../../../../../plugins/maps/public/kibana_services'; - -const localStorage = new Storage(window.localStorage); - -export class LayerPanel extends React.Component { - state = { - displayName: '', - immutableSourceProps: [], - leftJoinFields: null, - }; - - componentDidMount() { - this._isMounted = true; - this.loadDisplayName(); - this.loadImmutableSourceProperties(); - this.loadLeftJoinFields(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - loadDisplayName = async () => { - if (!this.props.selectedLayer) { - return; - } - - const displayName = await this.props.selectedLayer.getDisplayName(); - if (this._isMounted) { - this.setState({ displayName }); - } - }; - - loadImmutableSourceProperties = async () => { - if (!this.props.selectedLayer) { - return; - } - - const immutableSourceProps = await this.props.selectedLayer.getImmutableSourceProperties(); - if (this._isMounted) { - this.setState({ immutableSourceProps }); - } - }; - - async loadLeftJoinFields() { - if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) { - return; - } - - let leftJoinFields; - try { - const leftFieldsInstances = await this.props.selectedLayer.getLeftJoinFields(); - const leftFieldPromises = leftFieldsInstances.map(async field => { - return { - name: field.getName(), - label: await field.getLabel(), - }; - }); - leftJoinFields = await Promise.all(leftFieldPromises); - } catch (error) { - leftJoinFields = []; - } - if (this._isMounted) { - this.setState({ leftJoinFields }); - } - } - - _onSourceChange = ({ propName, value, newLayerType }) => { - this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); - }; - - _renderFilterSection() { - if (!this.props.selectedLayer.supportsElasticsearchFilters()) { - return null; - } - - return ( - - - - - - - ); - } - - _renderJoinSection() { - if (!this.props.selectedLayer.isJoinable()) { - return null; - } - - return ( - - - - - - - ); - } - - _renderSourceProperties() { - return this.state.immutableSourceProps.map(({ label, value, link }) => { - function renderValue() { - if (link) { - return ( - - {value} - - ); - } - return {value}; - } - return ( -

- {label} {renderValue()} -

- ); - }); - } - - render() { - const { selectedLayer } = this.props; - - if (!selectedLayer) { - return null; - } - - return ( - - - - - - - - - - - -

{this.state.displayName}

-
-
-
- -
- - - - {this._renderSourceProperties()} - - -
-
- -
-
- - - - - {this.props.selectedLayer.renderSourceSettingsEditor({ - onChange: this._onSourceChange, - })} - - {this._renderFilterSection()} - - {this._renderJoinSection()} - - -
-
- - - - -
-
- ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js deleted file mode 100644 index 350cb7028abee..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ /dev/null @@ -1,75 +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 { connect } from 'react-redux'; -import { MBMapContainer } from './view'; -import { - mapExtentChanged, - mapReady, - mapDestroyed, - setMouseCoordinates, - clearMouseCoordinates, - clearGoto, - setMapInitError, -} from '../../../actions/map_actions'; -import { - getLayerList, - getMapReady, - getGoto, - getScrollZoom, - isInteractiveDisabled, - isTooltipControlDisabled, - isViewControlHidden, -} from '../../../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; - -function mapStateToProps(state = {}) { - return { - isMapReady: getMapReady(state), - layerList: getLayerList(state), - goto: getGoto(state), - inspectorAdapters: getInspectorAdapters(state), - scrollZoom: getScrollZoom(state), - disableInteractive: isInteractiveDisabled(state), - disableTooltipControl: isTooltipControlDisabled(state), - disableTooltipControl: isTooltipControlDisabled(state), - hideViewControl: isViewControlHidden(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - extentChanged: e => { - dispatch(mapExtentChanged(e)); - }, - onMapReady: e => { - dispatch(clearGoto()); - dispatch(mapExtentChanged(e)); - dispatch(mapReady()); - }, - onMapDestroyed: () => { - dispatch(mapDestroyed()); - }, - setMouseCoordinates: ({ lat, lon }) => { - dispatch(setMouseCoordinates({ lat, lon })); - }, - clearMouseCoordinates: () => { - dispatch(clearMouseCoordinates()); - }, - clearGoto: () => { - dispatch(clearGoto()); - }, - setMapInitError(errorMessage) { - dispatch(setMapInitError(errorMessage)); - }, - }; -} - -const connectedMBMapContainer = connect(mapStateToProps, mapDispatchToProps, null, { - forwardRef: true, -})(MBMapContainer); -export { connectedMBMapContainer as MBMapContainer }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js deleted file mode 100644 index a1d1341b7c4f7..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js +++ /dev/null @@ -1,107 +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 _ from 'lodash'; -import { - loadSpriteSheetImageData, - addSpriteSheetToMapFromImageData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/maps/public/connected_components/map/mb/utils'; - -export { loadSpriteSheetImageData, addSpriteSheetToMapFromImageData }; - -export function removeOrphanedSourcesAndLayers(mbMap, layerList) { - const mbStyle = mbMap.getStyle(); - - const mbLayerIdsToRemove = []; - mbStyle.layers.forEach(mbLayer => { - const layer = layerList.find(layer => { - return layer.ownsMbLayerId(mbLayer.id); - }); - if (!layer) { - mbLayerIdsToRemove.push(mbLayer.id); - } - }); - mbLayerIdsToRemove.forEach(mbLayerId => mbMap.removeLayer(mbLayerId)); - - const mbSourcesToRemove = []; - for (const mbSourceId in mbStyle.sources) { - if (mbStyle.sources.hasOwnProperty(mbSourceId)) { - const layer = layerList.find(layer => { - return layer.ownsMbSourceId(mbSourceId); - }); - if (!layer) { - mbSourcesToRemove.push(mbSourceId); - } - } - } - mbSourcesToRemove.forEach(mbSourceId => mbMap.removeSource(mbSourceId)); -} - -/** - * This is function assumes only a single layer moved in the layerList, compared to mbMap - * It is optimized to minimize the amount of mbMap.moveLayer calls. - * @param mbMap - * @param layerList - */ -export function syncLayerOrderForSingleLayer(mbMap, layerList) { - if (!layerList || layerList.length === 0) { - return; - } - - const mbLayers = mbMap.getStyle().layers.slice(); - const layerIds = mbLayers.map(mbLayer => { - const layer = layerList.find(layer => layer.ownsMbLayerId(mbLayer.id)); - return layer.getId(); - }); - - const currentLayerOrderLayerIds = _.uniq(layerIds); - - const newLayerOrderLayerIdsUnfiltered = layerList.map(l => l.getId()); - const newLayerOrderLayerIds = newLayerOrderLayerIdsUnfiltered.filter(layerId => - currentLayerOrderLayerIds.includes(layerId) - ); - - let netPos = 0; - let netNeg = 0; - const movementArr = currentLayerOrderLayerIds.reduce((accu, id, idx) => { - const movement = newLayerOrderLayerIds.findIndex(newOId => newOId === id) - idx; - movement > 0 ? netPos++ : movement < 0 && netNeg++; - accu.push({ id, movement }); - return accu; - }, []); - if (netPos === 0 && netNeg === 0) { - return; - } - const movedLayerId = - (netPos >= netNeg && movementArr.find(l => l.movement < 0).id) || - (netPos < netNeg && movementArr.find(l => l.movement > 0).id); - const nextLayerIdx = newLayerOrderLayerIds.findIndex(layerId => layerId === movedLayerId) + 1; - - let nextMbLayerId; - if (nextLayerIdx === newLayerOrderLayerIds.length) { - nextMbLayerId = null; - } else { - const foundLayer = mbLayers.find(({ id: mbLayerId }) => { - const layerId = newLayerOrderLayerIds[nextLayerIdx]; - const layer = layerList.find(layer => layer.getId() === layerId); - return layer.ownsMbLayerId(mbLayerId); - }); - nextMbLayerId = foundLayer.id; - } - - const movedLayer = layerList.find(layer => layer.getId() === movedLayerId); - mbLayers.forEach(({ id: mbLayerId }) => { - if (movedLayer.ownsMbLayerId(mbLayerId)) { - mbMap.moveLayer(mbLayerId, nextMbLayerId); - } - }); -} - -export async function addSpritesheetToMap(json, imgUrl, mbMap) { - const imgData = await loadSpriteSheetImageData(imgUrl); - addSpriteSheetToMapFromImageData(json, imgData, mbMap); -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js deleted file mode 100644 index a36e1d7048e92..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ /dev/null @@ -1,322 +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 _ from 'lodash'; -import React from 'react'; -import { ResizeChecker } from '../../../../../../../../src/plugins/kibana_utils/public'; -import { - syncLayerOrderForSingleLayer, - removeOrphanedSourcesAndLayers, - addSpritesheetToMap, -} from './utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getGlyphUrl, isRetina } from '../../../../../../../plugins/maps/public/meta'; -import { - DECIMAL_DEGREES_PRECISION, - MAX_ZOOM, - MIN_ZOOM, - ZOOM_PRECISION, -} from '../../../../common/constants'; -import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; -import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; -import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; -import { spritesheet } from '@elastic/maki'; -import sprites1 from '@elastic/maki/dist/sprite@1.png'; -import sprites2 from '@elastic/maki/dist/sprite@2.png'; -import { DrawControl } from './draw_control'; -import { TooltipControl } from './tooltip_control'; -import { - clampToLatBounds, - clampToLonBounds, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInjectedVarFunc } from '../../../../../../../plugins/maps/public/kibana_services'; - -mapboxgl.workerUrl = mbWorkerUrl; -mapboxgl.setRTLTextPlugin(mbRtlPlugin); - -export class MBMapContainer extends React.Component { - state = { - prevLayerList: undefined, - hasSyncedLayerList: false, - mbMap: undefined, - }; - - static getDerivedStateFromProps(nextProps, prevState) { - const nextLayerList = nextProps.layerList; - if (nextLayerList !== prevState.prevLayerList) { - return { - prevLayerList: nextLayerList, - hasSyncedLayerList: false, - }; - } - - return null; - } - - componentDidMount() { - this._initializeMap(); - this._isMounted = true; - } - - componentDidUpdate() { - if (this.state.mbMap) { - // do not debounce syncing of map-state - this._syncMbMapWithMapState(); - this._debouncedSync(); - } - } - - componentWillUnmount() { - this._isMounted = false; - if (this._checker) { - this._checker.destroy(); - } - if (this.state.mbMap) { - this.state.mbMap.remove(); - this.state.mbMap = null; - } - this.props.onMapDestroyed(); - } - - _debouncedSync = _.debounce(() => { - if (this._isMounted) { - if (!this.state.hasSyncedLayerList) { - this.setState( - { - hasSyncedLayerList: true, - }, - () => { - this._syncMbMapWithLayerList(); - this._syncMbMapWithInspector(); - } - ); - } - } - }, 256); - - _getMapState() { - const zoom = this.state.mbMap.getZoom(); - const mbCenter = this.state.mbMap.getCenter(); - const mbBounds = this.state.mbMap.getBounds(); - return { - zoom: _.round(zoom, ZOOM_PRECISION), - center: { - lon: _.round(mbCenter.lng, DECIMAL_DEGREES_PRECISION), - lat: _.round(mbCenter.lat, DECIMAL_DEGREES_PRECISION), - }, - extent: { - minLon: _.round(mbBounds.getWest(), DECIMAL_DEGREES_PRECISION), - minLat: _.round(mbBounds.getSouth(), DECIMAL_DEGREES_PRECISION), - maxLon: _.round(mbBounds.getEast(), DECIMAL_DEGREES_PRECISION), - maxLat: _.round(mbBounds.getNorth(), DECIMAL_DEGREES_PRECISION), - }, - }; - } - - async _createMbMapInstance() { - return new Promise(resolve => { - const mbStyle = { - version: 8, - sources: {}, - layers: [], - }; - const glyphUrl = getGlyphUrl(); - if (glyphUrl) { - mbStyle.glyphs = glyphUrl; - } - - const options = { - attributionControl: false, - container: this.refs.mapContainer, - style: mbStyle, - scrollZoom: this.props.scrollZoom, - preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false), - interactive: !this.props.disableInteractive, - minZoom: MIN_ZOOM, - maxZoom: MAX_ZOOM, - }; - const initialView = _.get(this.props.goto, 'center'); - if (initialView) { - options.zoom = initialView.zoom; - options.center = { - lng: initialView.lon, - lat: initialView.lat, - }; - } else { - options.bounds = [-170, -60, 170, 75]; - } - const mbMap = new mapboxgl.Map(options); - mbMap.dragRotate.disable(); - mbMap.touchZoomRotate.disableRotation(); - if (!this.props.disableInteractive) { - mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); - } - - let emptyImage; - mbMap.on('styleimagemissing', e => { - if (emptyImage) { - mbMap.addImage(e.id, emptyImage); - } - }); - mbMap.on('load', () => { - emptyImage = new Image(); - - emptyImage.src = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; - emptyImage.crossOrigin = 'anonymous'; - resolve(mbMap); - }); - }); - } - - async _initializeMap() { - let mbMap; - try { - mbMap = await this._createMbMapInstance(); - } catch (error) { - this.props.setMapInitError(error.message); - return; - } - - if (!this._isMounted) { - return; - } - - this.setState({ mbMap }, () => { - this._loadMakiSprites(); - this._initResizerChecker(); - this._registerMapEventListeners(); - this.props.onMapReady(this._getMapState()); - }); - } - - _registerMapEventListeners() { - // moveend callback is debounced to avoid updating map extent state while map extent is still changing - // moveend is fired while the map extent is still changing in the following scenarios - // 1) During opening/closing of layer details panel, the EUI animation results in 8 moveend events - // 2) Setting map zoom and center from goto is done in 2 API calls, resulting in 2 moveend events - this.state.mbMap.on( - 'moveend', - _.debounce(() => { - this.props.extentChanged(this._getMapState()); - }, 100) - ); - // Attach event only if view control is visible, which shows lat/lon - if (!this.props.hideViewControl) { - const throttledSetMouseCoordinates = _.throttle(e => { - this.props.setMouseCoordinates({ - lat: e.lngLat.lat, - lon: e.lngLat.lng, - }); - }, 100); - this.state.mbMap.on('mousemove', throttledSetMouseCoordinates); - this.state.mbMap.on('mouseout', () => { - throttledSetMouseCoordinates.cancel(); // cancel any delayed setMouseCoordinates invocations - this.props.clearMouseCoordinates(); - }); - } - } - - _initResizerChecker() { - this._checker = new ResizeChecker(this.refs.mapContainer); - this._checker.on('resize', () => { - this.state.mbMap.resize(); - }); - } - - _loadMakiSprites() { - const sprites = isRetina() ? sprites2 : sprites1; - const json = isRetina() ? spritesheet[2] : spritesheet[1]; - addSpritesheetToMap(json, sprites, this.state.mbMap); - } - - _syncMbMapWithMapState = () => { - const { isMapReady, goto, clearGoto } = this.props; - - if (!isMapReady || !goto) { - return; - } - - clearGoto(); - - if (goto.bounds) { - //clamping ot -89/89 latitudes since Mapboxgl does not seem to handle bounds that contain the poles (logs errors to the console when using -90/90) - const lnLatBounds = new mapboxgl.LngLatBounds( - new mapboxgl.LngLat( - clampToLonBounds(goto.bounds.minLon), - clampToLatBounds(goto.bounds.minLat) - ), - new mapboxgl.LngLat( - clampToLonBounds(goto.bounds.maxLon), - clampToLatBounds(goto.bounds.maxLat) - ) - ); - //maxZoom ensure we're not zooming in too far on single points or small shapes - //the padding is to avoid too tight of a fit around edges - this.state.mbMap.fitBounds(lnLatBounds, { maxZoom: 17, padding: 16 }); - } else if (goto.center) { - this.state.mbMap.setZoom(goto.center.zoom); - this.state.mbMap.setCenter({ - lng: goto.center.lon, - lat: goto.center.lat, - }); - } - }; - - _syncMbMapWithLayerList = () => { - if (!this.props.isMapReady) { - return; - } - - removeOrphanedSourcesAndLayers(this.state.mbMap, this.props.layerList); - this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap)); - syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList); - }; - - _syncMbMapWithInspector = () => { - if (!this.props.isMapReady || !this.props.inspectorAdapters.map) { - return; - } - - const stats = { - center: this.state.mbMap.getCenter().toArray(), - zoom: this.state.mbMap.getZoom(), - }; - this.props.inspectorAdapters.map.setMapState({ - stats, - style: this.state.mbMap.getStyle(), - }); - }; - - render() { - let drawControl; - let tooltipControl; - if (this.state.mbMap) { - drawControl = ; - tooltipControl = !this.props.disableTooltipControl ? ( - - ) : null; - } - return ( -
- {drawControl} - {tooltipControl} -
- ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/_index.scss deleted file mode 100644 index 01aea403b27f0..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/_index.scss +++ /dev/null @@ -1,21 +0,0 @@ -@import './tools_control/index'; - -.mapToolbarOverlay { - position: absolute; - top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin - left: $euiSizeM; - z-index: 2; // Sit on top of mapbox controls shadow -} - -.mapToolbarOverlay__button { - @include size($euiSizeXL); - // sass-lint:disable-block no-important - background-color: $euiColorEmptyShade !important; - pointer-events: all; - - &:enabled, - &:enabled:hover, - &:enabled:focus { - @include euiBottomShadowLarge; - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js deleted file mode 100644 index 2b6fae26098be..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js +++ /dev/null @@ -1,38 +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 { connect } from 'react-redux'; -import { SetViewControl } from './set_view_control'; -import { setGotoWithCenter } from '../../../actions/map_actions'; -import { getMapZoom, getMapCenter } from '../../../selectors/map_selectors'; -import { closeSetView, openSetView } from '../../../actions/ui_actions'; -import { getIsSetViewOpen } from '../../../selectors/ui_selectors'; - -function mapStateToProps(state = {}) { - return { - isSetViewOpen: getIsSetViewOpen(state), - zoom: getMapZoom(state), - center: getMapCenter(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - onSubmit: ({ lat, lon, zoom }) => { - dispatch(closeSetView()); - dispatch(setGotoWithCenter({ lat, lon, zoom })); - }, - closeSetView: () => { - dispatch(closeSetView()); - }, - openSetView: () => { - dispatch(openSetView()); - }, - }; -} - -const connectedSetViewControl = connect(mapStateToProps, mapDispatchToProps)(SetViewControl); -export { connectedSetViewControl as SetViewControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_index.scss deleted file mode 100644 index cc1ab35039dac..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_index.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import './mixins'; - -@import './widget_overlay'; -@import './attribution_control/attribution_control'; -@import './layer_control/index'; -@import './view_control/view_control'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap deleted file mode 100644 index 560ebad89c50e..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap +++ /dev/null @@ -1,189 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LayerControl is rendered 1`] = ` - - - - - - -

- -

-
-
- - - - - -
-
- - - -
- - - - -
-`; - -exports[`LayerControl isLayerTOCOpen Should render expand button 1`] = ` - - - -`; - -exports[`LayerControl isLayerTOCOpen Should render expand button with error icon when layer has error 1`] = ` - - - -`; - -exports[`LayerControl isLayerTOCOpen Should render expand button with loading icon when layer is loading 1`] = ` - - - -`; - -exports[`LayerControl isReadOnly 1`] = ` - - - - - - -

- -

-
-
- - - - - -
-
- - - -
-
-`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss deleted file mode 100644 index 761ef9d17b4c2..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './layer_control'; -@import './layer_toc/toc_entry/toc_entry'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js deleted file mode 100644 index 04de5f71f5bfc..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js +++ /dev/null @@ -1,45 +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 { connect } from 'react-redux'; -import { LayerControl } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout, setIsLayerTOCOpen } from '../../../actions/ui_actions'; -import { setSelectedLayer } from '../../../actions/map_actions'; -import { - getIsReadOnly, - getIsLayerTOCOpen, - getFlyoutDisplay, -} from '../../../selectors/ui_selectors'; -import { getLayerList } from '../../../selectors/map_selectors'; - -function mapStateToProps(state = {}) { - return { - isReadOnly: getIsReadOnly(state), - isLayerTOCOpen: getIsLayerTOCOpen(state), - layerList: getLayerList(state), - isAddButtonActive: getFlyoutDisplay(state) === FLYOUT_STATE.NONE, - }; -} - -function mapDispatchToProps(dispatch) { - return { - showAddLayerWizard: async () => { - await dispatch(setSelectedLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.ADD_LAYER_WIZARD)); - }, - closeLayerTOC: () => { - dispatch(setIsLayerTOCOpen(false)); - }, - openLayerTOC: () => { - dispatch(setIsLayerTOCOpen(true)); - }, - }; -} - -const connectedLayerControl = connect(mapStateToProps, mapDispatchToProps)(LayerControl); -export { connectedLayerControl as LayerControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js deleted file mode 100644 index 588445d0b4992..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js +++ /dev/null @@ -1,69 +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 _ from 'lodash'; -import { connect } from 'react-redux'; -import { TOCEntry } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions'; -import { getIsReadOnly, getOpenTOCDetails } from '../../../../../selectors/ui_selectors'; -import { - fitToLayerExtent, - setSelectedLayer, - toggleLayerVisible, - removeTransientLayer, - cloneLayer, - removeLayer, -} from '../../../../../actions/map_actions'; - -import { - hasDirtyState, - getSelectedLayer, - isUsingSearch, -} from '../../../../../selectors/map_selectors'; - -function mapStateToProps(state = {}, ownProps) { - return { - isReadOnly: getIsReadOnly(state), - zoom: _.get(state, 'map.mapState.zoom', 0), - selectedLayer: getSelectedLayer(state), - hasDirtyStateSelector: hasDirtyState(state), - isLegendDetailsOpen: getOpenTOCDetails(state).includes(ownProps.layer.getId()), - isUsingSearch: isUsingSearch(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - openLayerPanel: async layerId => { - await dispatch(removeTransientLayer()); - await dispatch(setSelectedLayer(layerId)); - dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); - }, - toggleVisible: layerId => { - dispatch(toggleLayerVisible(layerId)); - }, - fitToBounds: layerId => { - dispatch(fitToLayerExtent(layerId)); - }, - cloneLayer: layerId => { - dispatch(cloneLayer(layerId)); - }, - removeLayer: layerId => { - dispatch(removeLayer(layerId)); - }, - hideTOCDetails: layerId => { - dispatch(hideTOCDetails(layerId)); - }, - showTOCDetails: layerId => { - dispatch(showTOCDetails(layerId)); - }, - }; -} - -const connectedTOCEntry = connect(mapStateToProps, mapDispatchToProps)(TOCEntry); -export { connectedTOCEntry as TOCEntry }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js deleted file mode 100644 index 537a676287042..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js +++ /dev/null @@ -1,162 +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 React, { Fragment } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiButton, - EuiTitle, - EuiSpacer, - EuiButtonIcon, - EuiToolTip, - EuiLoadingSpinner, -} from '@elastic/eui'; -import { LayerTOC } from './layer_toc'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -function renderExpandButton({ hasErrors, isLoading, onClick }) { - const expandLabel = i18n.translate('xpack.maps.layerControl.openLayerTOCButtonAriaLabel', { - defaultMessage: 'Expand layers panel', - }); - - if (isLoading) { - // Can not use EuiButtonIcon with spinner because spinner is a class and not an icon - return ( - - ); - } - - return ( - - ); -} - -export function LayerControl({ - isReadOnly, - isLayerTOCOpen, - showAddLayerWizard, - closeLayerTOC, - openLayerTOC, - layerList, - isAddButtonActive, -}) { - if (!isLayerTOCOpen) { - const hasErrors = layerList.some(layer => { - return layer.hasErrors(); - }); - const isLoading = layerList.some(layer => { - return layer.isLayerLoading(); - }); - - return ( - - {renderExpandButton({ hasErrors, isLoading, onClick: openLayerTOC })} - - ); - } - - let addLayer; - if (!isReadOnly) { - addLayer = ( - - - - - - - ); - } - - return ( - - - - - - -

- -

-
-
- - - - - -
-
- - - - -
- - {addLayer} -
- ); -} diff --git a/x-pack/legacy/plugins/maps/public/index.scss b/x-pack/legacy/plugins/maps/public/index.scss index b2ac514299d80..b2a228f01b921 100644 --- a/x-pack/legacy/plugins/maps/public/index.scss +++ b/x-pack/legacy/plugins/maps/public/index.scss @@ -1,17 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - /* GIS plugin styles */ -// Prefix all styles with "map" to avoid conflicts. -// Examples -// mapChart -// mapChart__legend -// mapChart__legend--small -// mapChart__legend-isLoading - -@import './main'; -@import './mapbox_hacks'; -@import './connected_components/index'; -@import './components/index'; -@import '../../../../plugins/maps/public/layers/index'; +@import '../../../../plugins/maps/public/index'; diff --git a/x-pack/legacy/plugins/maps/public/index.ts b/x-pack/legacy/plugins/maps/public/index.ts index 8555594e909d3..98db26859297b 100644 --- a/x-pack/legacy/plugins/maps/public/index.ts +++ b/x-pack/legacy/plugins/maps/public/index.ts @@ -15,15 +15,14 @@ import 'uiExports/embeddableActions'; import 'ui/autoload/all'; import 'react-vis/dist/style.css'; - -import './angular/services/gis_map_saved_object_loader'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../plugins/maps/public/angular/services/gis_map_saved_object_loader'; import './angular/map_controller'; import './routes'; // @ts-ignore -import { PluginInitializerContext } from 'kibana/public'; import { MapsPlugin } from './plugin'; -export const plugin = (initializerContext: PluginInitializerContext) => { +export const plugin = () => { return new MapsPlugin(); }; @@ -32,4 +31,4 @@ export { RenderTooltipContentParams, ITooltipProperty, } from '../../../../plugins/maps/public/layers/tooltips/tooltip_property'; -export { MapEmbeddable, MapEmbeddableInput } from './embeddable'; +export { MapEmbeddable, MapEmbeddableInput } from '../../../../plugins/maps/public/embeddable'; diff --git a/x-pack/legacy/plugins/maps/public/legacy.ts b/x-pack/legacy/plugins/maps/public/legacy.ts index 96d9e09c1d09a..bcbfca17755fb 100644 --- a/x-pack/legacy/plugins/maps/public/legacy.ts +++ b/x-pack/legacy/plugins/maps/public/legacy.ts @@ -7,10 +7,9 @@ import { npSetup, npStart } from 'ui/new_platform'; // @ts-ignore Untyped Module import { uiModules } from 'ui/modules'; -import { PluginInitializerContext } from 'kibana/public'; // eslint-disable-line import/order import { plugin } from '.'; -const pluginInstance = plugin({} as PluginInitializerContext); +const pluginInstance = plugin(); const setupPlugins = { __LEGACY: { diff --git a/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts b/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts deleted file mode 100644 index 00f788f267d4b..0000000000000 --- a/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts +++ /dev/null @@ -1,14 +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 { npSetup } from 'ui/new_platform'; -import { featureCatalogueEntry } from './feature_catalogue_entry'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register(featureCatalogueEntry); diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts index 71f1a30c1fbef..0123e32b6d3b9 100644 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -10,7 +10,7 @@ import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; // @ts-ignore import { wrapInI18nContext } from 'ui/i18n'; // @ts-ignore -import { MapListing } from './components/map_listing'; +import { MapListing } from '../../../../plugins/maps/public/components/map_listing'; // eslint-disable-line @kbn/eslint/no-restricted-paths // @ts-ignore import { bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, @@ -18,7 +18,6 @@ import { } from '../../../../plugins/maps/public/plugin'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; -import { featureCatalogueEntry } from './feature_catalogue_entry'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -57,8 +56,6 @@ export class MapsPlugin implements Plugin { }); bindNpSetupCoreAndPlugins(core, np); - - np.home.featureCatalogue.register(featureCatalogueEntry); } public start(core: CoreStart, plugins: MapsPluginStartDependencies) { diff --git a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js b/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js deleted file mode 100644 index 9dc07bcb5dc0e..0000000000000 --- a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js +++ /dev/null @@ -1,49 +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 { i18n } from '@kbn/i18n'; -import { APP_ID, APP_ICON, MAP_BASE_URL } from '../common/constants'; -import { - getInjectedVarFunc, - getVisualizations, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../plugins/maps/public/kibana_services'; -import { npSetup } from 'ui/new_platform'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { bindSetupCoreAndPlugins } from '../../../../plugins/maps/public/plugin'; - -bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); - -const showMapVisualizationTypes = getInjectedVarFunc()('showMapVisualizationTypes', false); - -const description = i18n.translate('xpack.maps.visTypeAlias.description', { - defaultMessage: 'Create and style maps with multiple layers and indices.', -}); - -const legacyMapVisualizationWarning = i18n.translate( - 'xpack.maps.visTypeAlias.legacyMapVizWarning', - { - defaultMessage: `Use the Maps app instead of Coordinate Map and Region Map. -The Maps app offers more functionality and is easier to use.`, - } -); - -getVisualizations().registerAlias({ - aliasUrl: MAP_BASE_URL, - name: APP_ID, - title: i18n.translate('xpack.maps.visTypeAlias.title', { - defaultMessage: 'Maps', - }), - description: showMapVisualizationTypes - ? `${description} ${legacyMapVisualizationWarning}` - : description, - icon: APP_ICON, - stage: 'production', -}); - -if (!showMapVisualizationTypes) { - getVisualizations().hideTypes(['region_map', 'tile_map']); -} diff --git a/x-pack/legacy/plugins/maps/public/routes.js b/x-pack/legacy/plugins/maps/public/routes.js index c082e0e1352c0..70c1c4a50efd4 100644 --- a/x-pack/legacy/plugins/maps/public/routes.js +++ b/x-pack/legacy/plugins/maps/public/routes.js @@ -6,15 +6,18 @@ import { i18n } from '@kbn/i18n'; import routes from 'ui/routes'; -import listingTemplate from './angular/listing_ng_wrapper.html'; -import mapTemplate from './angular/map.html'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import listingTemplate from '../../../../plugins/maps/public/angular/listing_ng_wrapper.html'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import mapTemplate from '../../../../plugins/maps/public/angular/map.html'; import { getSavedObjectsClient, getCoreChrome, getMapsCapabilities, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/maps/public/kibana_services'; -import { getMapsSavedObjectLoader } from './angular/services/gis_map_saved_object_loader'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getMapsSavedObjectLoader } from '../../../../plugins/maps/public/angular/services/gis_map_saved_object_loader'; routes.enable(); diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts deleted file mode 100644 index 8c99e0adcc14f..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts +++ /dev/null @@ -1,18 +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 { AnyAction } from 'redux'; -import { MapCenter } from '../../common/descriptor_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; - -export function getHiddenLayerIds(state: MapStoreState): string[]; - -export function getMapZoom(state: MapStoreState): number; - -export function getMapCenter(state: MapStoreState): MapCenter; - -export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[]; diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js deleted file mode 100644 index 72cc748617540..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js +++ /dev/null @@ -1,56 +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. - */ - -jest.mock('../../../../../plugins/maps/public/layers/vector_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/tiled_vector_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/blended_vector_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/heatmap_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/vector_tile_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/joins/inner_join', () => {}); -jest.mock('../../../../../plugins/maps/public/reducers/non_serializable_instances', () => ({ - getInspectorAdapters: () => { - return {}; - }, -})); -jest.mock('../../../../../plugins/maps/public/kibana_services', () => ({ - getTimeFilter: () => ({ - getTime: () => { - return { - to: 'now', - from: 'now-15m', - }; - }, - }), -})); - -import { getTimeFilters } from './map_selectors'; - -describe('getTimeFilters', () => { - it('should return timeFilters when contained in state', () => { - const state = { - map: { - mapState: { - timeFilters: { - to: '2001-01-01', - from: '2001-12-31', - }, - }, - }, - }; - expect(getTimeFilters(state)).toEqual({ to: '2001-01-01', from: '2001-12-31' }); - }); - - it('should return kibana time filters when not contained in state', () => { - const state = { - map: { - mapState: { - timeFilters: null, - }, - }, - }; - expect(getTimeFilters(state)).toEqual({ to: 'now', from: 'now-15m' }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts deleted file mode 100644 index fdf2a8ea0e4f3..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts +++ /dev/null @@ -1,19 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE, INDEXING_STAGE } from '../../../../../plugins/maps/public/reducers/ui'; - -export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; -export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; -export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; -export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; -export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; -export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; -export const getIndexingStage = ({ ui }: MapStoreState): INDEXING_STAGE | null => - ui.importIndexingStage; diff --git a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js index c5522b7ba21c5..e2a758075155a 100644 --- a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js +++ b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js @@ -5,7 +5,10 @@ */ import _ from 'lodash'; -import { DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../common/constants'; +import { + DEFAULT_MAX_RESULT_WINDOW, + DEFAULT_MAX_INNER_RESULT_WINDOW, +} from '../../../../../plugins/maps/common/constants'; export function getIndexPatternSettings(indicesSettingsResp) { let maxResultWindow = Infinity; diff --git a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js index 01a1ba2703cba..c152f5bfffc31 100644 --- a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js +++ b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js @@ -5,7 +5,10 @@ */ import { getIndexPatternSettings } from './get_index_pattern_settings'; -import { DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../common/constants'; +import { + DEFAULT_MAX_RESULT_WINDOW, + DEFAULT_MAX_INNER_RESULT_WINDOW, +} from '../../../../../plugins/maps/common/constants'; describe('max_result_window and max_inner_result_window are not set', () => { test('Should provide default values when values not set', () => { diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts index 652bb83a0d781..d34e306d1fff9 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts @@ -9,7 +9,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SavedObjectsClientContract } from 'src/core/server'; import { getMapsTelemetry } from '../maps_telemetry'; // @ts-ignore -import { TELEMETRY_TYPE } from '../../../common/constants'; +import { TELEMETRY_TYPE } from '../../../../../../plugins/maps/common/constants'; export function registerMapsUsageCollector( usageCollection: UsageCollectionSetup, diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 27c0211446e85..4610baabad3fe 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -16,8 +16,8 @@ import { ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE, TELEMETRY_TYPE, -} from '../../common/constants'; -import { LayerDescriptor } from '../../common/descriptor_types'; +} from '../../../../../plugins/maps/common/constants'; +import { LayerDescriptor } from '../../../../../plugins/maps/common/descriptor_types'; import { MapSavedObject } from '../../../../../plugins/maps/common/map_saved_object_type'; interface IStats { diff --git a/x-pack/legacy/plugins/maps/server/plugin.js b/x-pack/legacy/plugins/maps/server/plugin.js index 25c552433e9f8..79f3dcf76b82e 100644 --- a/x-pack/legacy/plugins/maps/server/plugin.js +++ b/x-pack/legacy/plugins/maps/server/plugin.js @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { APP_ID, APP_ICON, createMapPath, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { + APP_ID, + APP_ICON, + createMapPath, + MAP_SAVED_OBJECT_TYPE, +} from '../../../../plugins/maps/common/constants'; import { getEcommerceSavedObjects } from './sample_data/ecommerce_saved_objects'; import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 20e022001577a..d49f9827e3ea0 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -21,7 +21,7 @@ import { GIS_API_PATH, EMS_SPRITES_PATH, INDEX_SETTINGS_API_PATH, -} from '../common/constants'; +} from '../../../../plugins/maps/common/constants'; import { EMSClient } from '@elastic/ems-client'; import fetch from 'node-fetch'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/README.md b/x-pack/legacy/plugins/monitoring/README.md index 3659f4d2fa0f7..e9ececa8c6350 100644 --- a/x-pack/legacy/plugins/monitoring/README.md +++ b/x-pack/legacy/plugins/monitoring/README.md @@ -72,8 +72,8 @@ cluster. 1. Set the Kibana config: ``` % cat config/kibana.dev.yml - xpack.monitoring.elasticsearch: - url: "http://localhost:9210" + monitoring.ui.elasticsearch: + hosts: "http://localhost:9210" username: "kibana" password: "changeme" ``` diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js index f3a888bf9e905..a48fbc51341f1 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { Status } from './status'; import { formatMetric } from '../../../lib/format_number'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { formatTimestampToDuration } from '../../../../common'; import { i18n } from '@kbn/i18n'; import { APM_SYSTEM_ID } from '../../../../common/constants'; @@ -56,7 +57,10 @@ function getColumns(setupMode) { return ( - + {name} {setupModeStatus} diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js index dfc9117ef48bc..a20728eb9a58f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js +++ b/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js @@ -19,13 +19,13 @@ import { formatMetric } from 'plugins/monitoring/lib/format_number'; import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; import { i18n } from '@kbn/i18n'; import { BEATS_SYSTEM_ID } from '../../../../common/constants'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { SetupModeBadge } from '../../setup_mode/badge'; import { FormattedMessage } from '@kbn/i18n/react'; export class Listing extends PureComponent { getColumns() { - const { kbnUrl, scope } = this.props.angular; const setupMode = this.props.setupMode; return [ @@ -59,11 +59,7 @@ export class Listing extends PureComponent { return (
{ - scope.$evalAsync(() => { - kbnUrl.changePath(`/beats/beat/${beat.uuid}`); - }); - }} + href={getSafeForExternalLink(`#/beats/beat/${beat.uuid}`)} data-test-subj={`beatLink-${name}`} > {name} diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 7b08c89f53881..48ff8edaafe60 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -32,6 +32,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from '../../logs/reason'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; const calculateShards = shards => { @@ -168,7 +169,7 @@ export function ElasticsearchPanel(props) { const showMlJobs = () => { // if license doesn't support ML, then `ml === null` if (props.ml) { - const gotoURL = '#/elasticsearch/ml_jobs'; + const gotoURL = getSafeForExternalLink('#/elasticsearch/ml_jobs'); return ( <> diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js index c6fb386c755f3..012c81e63931e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js @@ -6,6 +6,7 @@ import React from 'react'; import moment from 'moment-timezone'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { capitalize } from 'lodash'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,7 +19,7 @@ export function LicenseText({ license, showLicenseExpiration }) { } return ( - + { return ( - + {shardId} ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js index f8dd7b0af7a17..a73dfbf8cd321 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js @@ -8,6 +8,7 @@ import React from 'react'; import { capitalize } from 'lodash'; import { LARGE_FLOAT, LARGE_BYTES, LARGE_ABBREVIATED } from '../../../../common/formatting'; import { formatMetric } from '../../../lib/format_number'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ElasticsearchStatusIcon } from '../status_icon'; import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringTable } from '../../table'; @@ -34,7 +35,10 @@ const columns = [ sortable: true, render: value => (
- + {value}
diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index d9cf29f73ce0d..9e6252315f429 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -7,6 +7,7 @@ import React, { Fragment } from 'react'; import { NodeStatusIcon } from '../node'; import { extractIp } from '../../../lib/extract_ip'; // TODO this is only used for elasticsearch nodes summary / node detail, so it should be moved to components/elasticsearch/nodes/lib +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringSSPTable } from '../../table'; import { MetricCell, OfflineCell } from './cells'; @@ -75,7 +76,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { render: (value, node) => { let nameLink = ( {value} diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js index 7a9d096437d52..27598bee6d841 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js @@ -8,13 +8,14 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; import { Snapshot } from './snapshot'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; export const RecoveryIndex = props => { const { name, shard, relocationType } = props; return (
- {name} + {name}
{ return (
{name} diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap index 43ff42b6a80e8..fa266b30fcb94 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap @@ -192,6 +192,7 @@ exports[`ExplainCollectionEnabled should explain about xpack.monitoring.collecti isCopyable={false} paddingSize="l" transparentBackground={false} + whiteSpace="pre-wrap" > { diff --git a/x-pack/legacy/plugins/monitoring/public/lib/get_safe_for_external_link.ts b/x-pack/legacy/plugins/monitoring/public/lib/get_safe_for_external_link.ts new file mode 100644 index 0000000000000..6f3e398c1414f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/lib/get_safe_for_external_link.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function getSafeForExternalLink(url: string) { + return `${url.split('?')[0]}?${location.hash.split('?')[1]}`; +} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts index b506784bf15ee..a047c25c2b1d7 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts @@ -26,6 +26,7 @@ import { import { PromiseServiceCreator } from './providers/promises'; // @ts-ignore import { PrivateProvider } from './providers/private'; +import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; type IPrivate = (provider: (...injectable: any[]) => T) => T; @@ -134,7 +135,8 @@ function createHrefModule(core: AppMountContext['core']) { pre: (_$scope, _$el, $attr) => { $attr.$observe(name, val => { if (val) { - $attr.$set('href', core.http.basePath.prepend(val as string)); + const url = getSafeForExternalLink(val as string); + $attr.$set('href', core.http.basePath.prepend(url)); } }); }, diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 6e03583dda69f..d572561944a76 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -8,27 +8,7 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; import { Root } from 'joi'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { savedObjectMappings } from '../../../plugins/siem/server/saved_objects'; - -import { - APP_ID, - APP_NAME, - DEFAULT_INDEX_KEY, - DEFAULT_ANOMALY_SCORE, - DEFAULT_SIEM_TIME_RANGE, - DEFAULT_SIEM_REFRESH_INTERVAL, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_FROM, - DEFAULT_TO, - ENABLE_NEWS_FEED_SETTING, - NEWS_FEED_URL_SETTING, - NEWS_FEED_URL_SETTING_DEFAULT, - IP_REPUTATION_LINKS_SETTING, - IP_REPUTATION_LINKS_SETTING_DEFAULT, - DEFAULT_INDEX_PATTERN, -} from '../../../plugins/siem/common/constants'; +import { APP_ID, APP_NAME } from '../../../plugins/siem/common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -63,101 +43,6 @@ export const siem = (kibana: any) => { category: DEFAULT_APP_CATEGORIES.security, }, ], - uiSettingDefaults: { - [DEFAULT_SIEM_REFRESH_INTERVAL]: { - type: 'json', - name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', { - defaultMessage: 'Time filter refresh interval', - }), - value: `{ - "pause": ${DEFAULT_INTERVAL_PAUSE}, - "value": ${DEFAULT_INTERVAL_VALUE} -}`, - description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', { - defaultMessage: - '

Default refresh interval for the SIEM time filter, in milliseconds.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_SIEM_TIME_RANGE]: { - type: 'json', - name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', { - defaultMessage: 'Time filter period', - }), - value: `{ - "from": "${DEFAULT_FROM}", - "to": "${DEFAULT_TO}" -}`, - description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', { - defaultMessage: '

Default period of time in the SIEM time filter.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_INDEX_KEY]: { - name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { - defaultMessage: 'Elasticsearch indices', - }), - value: DEFAULT_INDEX_PATTERN, - description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', { - defaultMessage: - '

Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_ANOMALY_SCORE]: { - name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', { - defaultMessage: 'Anomaly threshold', - }), - value: 50, - type: 'number', - description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', { - defaultMessage: - '

Value above which Machine Learning job anomalies are displayed in the SIEM app.

Valid values: 0 to 100.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [ENABLE_NEWS_FEED_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', { - defaultMessage: 'News feed', - }), - value: true, - description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', { - defaultMessage: '

Enables the News feed

', - }), - type: 'boolean', - category: ['siem'], - requiresPageReload: true, - }, - [NEWS_FEED_URL_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', { - defaultMessage: 'News feed URL', - }), - value: NEWS_FEED_URL_SETTING_DEFAULT, - description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', { - defaultMessage: '

News feed content will be retrieved from this URL

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [IP_REPUTATION_LINKS_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', { - defaultMessage: 'IP Reputation Links', - }), - value: IP_REPUTATION_LINKS_SETTING_DEFAULT, - type: 'json', - description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', { - defaultMessage: - 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', - }), - category: ['siem'], - requiresPageReload: true, - }, - }, - mappings: savedObjectMappings, }, config(Joi: Root) { return Joi.object() diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index 3a93beef963a0..c9b3f8a37a7ea 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -7,12 +7,10 @@ "scripts": {}, "devDependencies": { "@types/lodash": "^4.14.110", - "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^12.1.1" + "@types/js-yaml": "^3.12.1" }, "dependencies": { "lodash": "^4.17.15", - "react-beautiful-dnd": "^12.2.0", "react-markdown": "^4.0.6" } } diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx index 2fb270c284000..e2078bb2473f5 100644 --- a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx @@ -30,7 +30,7 @@ const RoundedBadge = styled(EuiBadge)` .euiBadge__text { text-overflow: clip; } -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any RoundedBadge.displayName = 'RoundedBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index b3811d05eea04..cea900f7bccf9 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -118,7 +118,7 @@ DefaultDraggable.displayName = 'DefaultDraggable'; export const Badge = styled(EuiBadge)` vertical-align: top; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx index eff4769944765..ef2cd85667408 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx @@ -20,7 +20,7 @@ import * as i18n from '../translations'; const FlowBadge = styled(EuiBadge)` height: 45px; min-width: 85px; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any const EuiFlexGroupStyled = styled(EuiFlexGroup)` margin: 0 auto; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index 44abe5b679c8e..b0f6494e2d663 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -25,7 +25,7 @@ export const Badge = styled(EuiBadge)` right: 0%; top: 0%; border-bottom-left-radius: 5px; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx index f88ffc3f3c6c4..acbb337a7c2ab 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx @@ -54,12 +54,13 @@ LinkBack.displayName = 'LinkBack'; const Badge = styled(EuiBadge)` letter-spacing: 0; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any Badge.displayName = 'Badge'; interface BackOptions { href: LinkIconProps['href']; text: LinkIconProps['children']; + dataTestSubj?: string; } export interface HeaderPageProps extends HeaderProps { @@ -91,7 +92,11 @@ const HeaderPageComponent: React.FC = ({ {backOptions && ( - + {backOptions.text} diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx index 59039ddd6a23b..47dd0dc55d703 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx @@ -19,7 +19,7 @@ StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; const Badge = styled(EuiBadge)` letter-spacing: 0; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any Badge.displayName = 'Badge'; interface Props { diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx index ba5874d42d515..36f57c46c1628 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx @@ -50,12 +50,14 @@ export interface LinkIconProps extends LinkProps { children: string; iconSize?: IconSize; iconType: IconType; + dataTestSubj?: string; } export const LinkIcon = React.memo( ({ children, color, + dataTestSubj, disabled, href, iconSide = 'left', @@ -67,6 +69,7 @@ export const LinkIcon = React.memo( { expect(store.getState().inputs.global.timerange.kind).toBe('relative'); }); - test('Make Sure it is last 15 minutes date', () => { - expect(store.getState().inputs.global.timerange.fromStr).toBe('now-15m'); + test('Make Sure it is last 24 hours date', () => { + expect(store.getState().inputs.global.timerange.fromStr).toBe('now-24h'); expect(store.getState().inputs.global.timerange.toStr).toBe('now'); }); @@ -180,7 +180,7 @@ describe('SIEM Super Date Picker', () => { ).toBe('Today'); }); - test('Today and Last 15 minutes are in Recently used date ranges', () => { + test('Today and Last 24 hours are in Recently used date ranges', () => { wrapper .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') .first() @@ -198,7 +198,7 @@ describe('SIEM Super Date Picker', () => { .find('div.euiQuickSelectPopover__section') .at(1) .text() - ).toBe('Last 15 minutesToday'); + ).toBe('Last 24 hoursToday'); }); test('Make sure that it does not add any duplicate if you click again on today', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 24c52f3372d62..d05ab6bcc378f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -30,7 +30,7 @@ SignatureFlexItem.displayName = 'SignatureFlexItem'; const Badge = styled(EuiBadge)` vertical-align: top; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 39c21c4ffa33b..31525a4904bc2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -21,7 +21,7 @@ import * as i18n from './translations'; const Badge = styled(EuiBadge)` vertical-align: top; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 56639f90c1464..ddb07b4636b88 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -26,7 +26,7 @@ const BadgeHighlighted = styled(EuiBadge)` margin: 0 5px 0 5px; maxwidth: 85px; minwidth: 85px; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any BadgeHighlighted.displayName = 'BadgeHighlighted'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx index 8e4665acc2c26..a5525df5ef3c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx @@ -44,7 +44,7 @@ const ProviderBadgeStyled = styled(EuiBadge)` margin-right: 0; margin-left: 4px; } -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any ProviderBadgeStyled.displayName = 'ProviderBadgeStyled'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 663b3dd501341..0ae952412a973 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -56,7 +56,7 @@ DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; const NumberProviderAndBadge = styled(EuiBadge)` margin: 0px 5px; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any NumberProviderAndBadge.displayName = 'NumberProviderAndBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx index e63bce388ae80..c5aea833a4b2f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx @@ -12,9 +12,13 @@ import routeData from 'react-router'; import { InsertTimelinePopoverComponent } from './'; const mockDispatch = jest.fn(); -jest.mock('react-redux', () => ({ - useDispatch: () => mockDispatch, -})); +jest.mock('react-redux', () => { + const reactRedux = jest.requireActual('react-redux'); + return { + ...reactRedux, + useDispatch: () => mockDispatch, + }; +}); const mockLocation = { pathname: '/apath', hash: '', diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index 6f7e1f782d3f6..bf953cfd006aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -39,7 +39,7 @@ export const newTimelineToolTip = 'Create a new timeline'; const NotesCountBadge = styled(EuiBadge)` margin-left: 5px; -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any NotesCountBadge.displayName = 'NotesCountBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx index 49e3bc4017a10..639d30bbe7bb9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -147,7 +147,7 @@ const SelectableTimelineComponent: React.FC = ({ - + {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx index 4f5655cc9f221..693a7175ebc3e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx @@ -5,6 +5,9 @@ */ import { KibanaServices } from '../../lib/kibana'; + +import { CASES_URL } from '../../../../../../plugins/case/common/constants'; + import { deleteCases, getActionLicense, @@ -22,6 +25,7 @@ import { pushCase, pushToService, } from './api'; + import { actionLicenses, allCases, @@ -44,7 +48,7 @@ import { caseUserActionsSnake, casesStatusSnake, } from './mock'; -import { CASES_URL } from './constants'; + import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 12b4c80a2dd89..b745361632419 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -20,6 +20,21 @@ import { ActionTypeExecutorResult, } from '../../../../../../plugins/case/common/api'; +import { + CASE_STATUS_URL, + CASES_URL, + CASE_TAGS_URL, + CASE_REPORTERS_URL, + ACTION_TYPES_URL, + ACTION_URL, +} from '../../../../../../plugins/case/common/constants'; + +import { + getCaseDetailsUrl, + getCaseUserActionUrl, + getCaseCommentsUrl, +} from '../../../../../../plugins/case/common/api/helpers'; + import { KibanaServices } from '../../lib/kibana'; import { @@ -33,8 +48,6 @@ import { CaseUserActions, } from './types'; -import { CASES_URL } from './constants'; - import { convertToCamelCase, convertAllCasesToCamel, @@ -54,7 +67,7 @@ export const getCase = async ( includeComments: boolean = true, signal: AbortSignal ): Promise => { - const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { + const response = await KibanaServices.get().http.fetch(getCaseDetailsUrl(caseId), { method: 'GET', query: { includeComments, @@ -65,18 +78,15 @@ export const getCase = async ( }; export const getCasesStatus = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch( - `${CASES_URL}/status`, - { - method: 'GET', - signal, - } - ); + const response = await KibanaServices.get().http.fetch(CASE_STATUS_URL, { + method: 'GET', + signal, + }); return convertToCamelCase(decodeCasesStatusResponse(response)); }; export const getTags = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(`${CASES_URL}/tags`, { + const response = await KibanaServices.get().http.fetch(CASE_TAGS_URL, { method: 'GET', signal, }); @@ -84,7 +94,7 @@ export const getTags = async (signal: AbortSignal): Promise => { }; export const getReporters = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(`${CASES_URL}/reporters`, { + const response = await KibanaServices.get().http.fetch(CASE_REPORTERS_URL, { method: 'GET', signal, }); @@ -96,7 +106,7 @@ export const getCaseUserActions = async ( signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - `${CASES_URL}/${caseId}/user_actions`, + getCaseUserActionUrl(caseId), { method: 'GET', signal, @@ -193,14 +203,11 @@ export const patchComment = async ( version: string, signal: AbortSignal ): Promise => { - const response = await KibanaServices.get().http.fetch( - `${CASES_URL}/${caseId}/comments`, - { - method: 'PATCH', - body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), - signal, - } - ); + const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), { + method: 'PATCH', + body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), + signal, + }); return convertToCamelCase(decodeCaseResponse(response)); }; @@ -219,7 +226,7 @@ export const pushCase = async ( signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - `${CASES_URL}/${caseId}/_push`, + `${getCaseDetailsUrl(caseId)}/_push`, { method: 'POST', body: JSON.stringify(push), @@ -235,7 +242,7 @@ export const pushToService = async ( signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - `/api/action/${connectorId}/_execute`, + `${ACTION_URL}/${connectorId}/_execute`, { method: 'POST', body: JSON.stringify({ params: casePushParams }), @@ -251,7 +258,7 @@ export const pushToService = async ( }; export const getActionLicense = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(`/api/action/types`, { + const response = await KibanaServices.get().http.fetch(ACTION_TYPES_URL, { method: 'GET', signal, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts index c24081c777a96..85e472811c93b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts @@ -13,26 +13,27 @@ import { } from '../../../../../../../plugins/case/common/api'; import { KibanaServices } from '../../../lib/kibana'; -import { CASES_CONFIGURE_URL } from '../constants'; +import { + CASE_CONFIGURE_CONNECTORS_URL, + CASE_CONFIGURE_URL, +} from '../../../../../../../plugins/case/common/constants'; + import { ApiProps } from '../types'; import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; import { CaseConfigure } from './types'; export const fetchConnectors = async ({ signal }: ApiProps): Promise => { - const response = await KibanaServices.get().http.fetch( - `${CASES_CONFIGURE_URL}/connectors/_find`, - { - method: 'GET', - signal, - } - ); + const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { + method: 'GET', + signal, + }); return response; }; export const getCaseConfigure = async ({ signal }: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch( - CASES_CONFIGURE_URL, + CASE_CONFIGURE_URL, { method: 'GET', signal, @@ -51,7 +52,7 @@ export const postCaseConfigure = async ( signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - CASES_CONFIGURE_URL, + CASE_CONFIGURE_URL, { method: 'POST', body: JSON.stringify(caseConfiguration), @@ -68,7 +69,7 @@ export const patchCaseConfigure = async ( signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - CASES_CONFIGURE_URL, + CASE_CONFIGURE_URL, { method: 'PATCH', body: JSON.stringify(caseConfiguration), diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index ab8dc98db4f64..d8bb499ed7922 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -4,7 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const CASES_URL = `/api/cases`; -export const CASES_CONFIGURE_URL = `/api/cases/configure`; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx index 0a30329baf68d..f74c2bad1019e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx @@ -72,104 +72,108 @@ describe('useRuleStatus', () => { cleanup(); }); - test('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - expect(result.current).toEqual([true, null, null]); - }); - }); - - test('fetch rule status', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual([ - false, - { - current_status: { - alert_id: 'alertId', - last_failure_at: null, - last_failure_message: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_success_message: 'it is a success', - status: 'succeeded', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - gap: null, - bulk_create_time_durations: ['2235.01'], - search_after_time_durations: ['616.97'], - last_look_back_date: '2020-03-19T00:32:07.996Z', - }, - failures: [], - }, - result.current[2], - ]); - }); - }); - - test('re-fetch rule status', async () => { - const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - if (result.current[2]) { - result.current[2]('myOwnRuleID'); - } - await waitForNextUpdate(); - expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2); - }); - }); - - test('init rules statuses', async () => { - const payload = [testRule]; - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRulesStatuses(payload) - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ loading: false, rulesStatuses: [] }); + describe('useRuleStatus', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + expect(result.current).toEqual([true, null, null]); + }); }); - }); - test('fetch rules statuses', async () => { - const payload = [testRule]; - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRulesStatuses(payload) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - rulesStatuses: [ + test('fetch rule status', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, { current_status: { alert_id: 'alertId', - bulk_create_time_durations: ['2235.01'], - gap: null, last_failure_at: null, last_failure_message: null, - last_look_back_date: '2020-03-19T00:32:07.996Z', last_success_at: 'mm/dd/yyyyTHH:MM:sssz', last_success_message: 'it is a success', - search_after_time_durations: ['616.97'], status: 'succeeded', status_date: 'mm/dd/yyyyTHH:MM:sssz', + gap: null, + bulk_create_time_durations: ['2235.01'], + search_after_time_durations: ['616.97'], + last_look_back_date: '2020-03-19T00:32:07.996Z', }, failures: [], - id: '12345678987654321', - activate: true, - name: 'Test rule', }, - ], + result.current[2], + ]); + }); + }); + + test('re-fetch rule status', async () => { + const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current[2]) { + result.current[2]('myOwnRuleID'); + } + await waitForNextUpdate(); + expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('useRulesStatuses', () => { + test('init rules statuses', async () => { + const payload = [testRule]; + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRulesStatuses(payload) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ loading: false, rulesStatuses: [] }); + }); + }); + + test('fetch rules statuses', async () => { + const payload = [testRule]; + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRulesStatuses(payload) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + rulesStatuses: [ + { + current_status: { + alert_id: 'alertId', + bulk_create_time_durations: ['2235.01'], + gap: null, + last_failure_at: null, + last_failure_message: null, + last_look_back_date: '2020-03-19T00:32:07.996Z', + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_success_message: 'it is a success', + search_after_time_durations: ['616.97'], + status: 'succeeded', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + }, + failures: [], + id: '12345678987654321', + activate: true, + name: 'Test rule', + }, + ], + }); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts index 9d2ac29bc47d7..cc01edcfaab11 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts @@ -3,6 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); export const mockFormHook = { isSubmitted: false, isSubmitting: false, @@ -35,3 +39,5 @@ export const getFormMock = (sampleData: any) => ({ }), getFormData: () => sampleData, }); + +export const useFormMock = useForm as jest.Mock; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index e48e5cb0c5959..9c2a7fc07f2d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -126,10 +126,9 @@ export const getCasesColumns = ( render: (createdAt: Case['createdAt']) => { if (createdAt != null) { return ( - + + + ); } return getEmptyTagValue(); @@ -142,10 +141,9 @@ export const getCasesColumns = ( render: (closedAt: Case['closedAt']) => { if (closedAt != null) { return ( - + + + ); } return getEmptyTagValue(); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index 58d0c1b0faaf3..eb5bca6cc57ff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -125,8 +125,9 @@ describe('AllCases', () => { wrapper .find(`[data-test-subj="case-table-column-createdAt"]`) .first() + .childAt(0) .prop('value') - ).toEqual(useGetCasesMockState.data.cases[0].createdAt); + ).toBe(useGetCasesMockState.data.cases[0].createdAt); expect( wrapper .find(`[data-test-subj="case-table-case-count"]`) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index a6eca717a82a3..9dd90074a2e7b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -313,6 +313,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { (({ userCanCrud }) => { (({ userCanCrud }) => { fill href={getCreateCaseUrl(urlSearch)} iconType="plusInCircle" + data-test-subj="createNewCaseBtn" > {i18n.CREATE_TITLE} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 3cf0405f40637..01b9bc42f8e91 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -216,6 +216,7 @@ export const CaseComponent = React.memo( backOptions={{ href: getCaseUrl(search), text: i18n.BACK_TO_ALL, + dataTestSubj: 'backToCases', }} data-test-subj="case-view-title" titleNode={ diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx index d480744fc932a..0897be6310fa2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx @@ -14,6 +14,8 @@ import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostCase } from '../../../../containers/case/use_post_case'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../../../containers/case/use_post_case'); import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; @@ -22,6 +24,14 @@ import { SiemPageName } from '../../../home/types'; jest.mock( '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); export const useFormMock = useForm as jest.Mock; @@ -40,9 +50,11 @@ const defaultInsertTimeline = { handleCursorChange, handleOnTimelineChange, }; + +const sampleTags = ['coke', 'pepsi']; const sampleData = { description: 'what a great description', - tags: ['coke', 'pepsi'], + tags: sampleTags, title: 'what a cool title', }; const defaultPostCase = { @@ -52,14 +64,28 @@ const defaultPostCase = { postCase, }; describe('Create case', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const fetchTags = jest.fn(); const formHookMock = getFormMock(sampleData); - beforeEach(() => { jest.resetAllMocks(); useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCaseMock.mockImplementation(() => defaultPostCase); useFormMock.mockImplementation(() => ({ form: formHookMock })); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); }); it('should post case on submit click', async () => { @@ -118,4 +144,19 @@ describe('Create case', () => { ); expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 53b792bb9b5eb..0f819f961b396 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -3,7 +3,7 @@ * 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, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -15,8 +15,16 @@ import { import styled, { css } from 'styled-components'; import { Redirect } from 'react-router-dom'; +import { isEqual } from 'lodash/fp'; import { CasePostRequest } from '../../../../../../../../plugins/case/common/api'; -import { Field, Form, getUseField, useForm, UseField } from '../../../../shared_imports'; +import { + Field, + Form, + getUseField, + useForm, + UseField, + FormDataProvider, +} from '../../../../shared_imports'; import { usePostCase } from '../../../../containers/case/use_post_case'; import { schema } from './schema'; import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; @@ -24,6 +32,7 @@ import { useInsertTimeline } from '../../../../components/timeline/insert_timeli import * as i18n from '../../translations'; import { SiemPageName } from '../../../home/types'; import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; export const CommonUseField = getUseField({ component: Field }); @@ -59,6 +68,21 @@ export const Create = React.memo(() => { options: { stripEmptyFields: false }, schema, }); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( form, 'description' @@ -108,6 +132,8 @@ export const Create = React.memo(() => { fullWidth: true, placeholder: '', disabled: isLoading, + options, + noSuggestions: false, }, }} /> @@ -131,6 +157,25 @@ export const Create = React.memo(() => { }} /> + + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} + (({ caseCount, caseStatus, isLoading }) => { - const openClosedStats = useMemo( - () => [ - { - title: caseStatus === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, - description: isLoading ? : caseCount ?? 'N/A', - }, - ], - [caseCount, caseStatus, isLoading] - ); - return ; -}); +export const OpenClosedStats = React.memo( + ({ caseCount, caseStatus, isLoading, dataTestSubj }) => { + const openClosedStats = useMemo( + () => [ + { + title: caseStatus === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, + description: isLoading ? : caseCount ?? 'N/A', + }, + ], + [caseCount, caseStatus, isLoading, dataTestSubj] + ); + return ( + + ); + } +); OpenClosedStats.displayName = 'OpenClosedStats'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx index 8ad2f8f8cb737..b5627376ac8aa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx @@ -6,17 +6,26 @@ import React from 'react'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { TagList } from './'; import { getFormMock } from '../__mock__/form'; import { TestProviders } from '../../../../mock'; import { wait } from '../../../../lib/helpers'; import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; -import { act } from 'react-dom/test-utils'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; jest.mock( '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); const onSubmit = jest.fn(); const defaultProps = { disabled: false, @@ -26,11 +35,27 @@ const defaultProps = { }; describe('TagList ', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ const sampleTags = ['coke', 'pepsi']; + const fetchTags = jest.fn(); const formHookMock = getFormMock({ tags: sampleTags }); beforeEach(() => { jest.resetAllMocks(); (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); + + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); }); it('Renders no tags, and then edit', () => { const wrapper = mount( @@ -80,6 +105,23 @@ describe('TagList ', () => { expect(onSubmit).toBeCalledWith(sampleTags); }); }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); it('Cancels on cancel', async () => { const props = { ...defaultProps, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index 9bac000b93235..c61feab0bab98 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiText, EuiHorizontalRule, @@ -17,10 +17,12 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import { isEqual } from 'lodash/fp'; import * as i18n from './translations'; -import { Form, useForm } from '../../../../shared_imports'; +import { Form, FormDataProvider, useForm } from '../../../../shared_imports'; import { schema } from './schema'; import { CommonUseField } from '../create'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; interface TagListProps { disabled?: boolean; @@ -54,6 +56,22 @@ export const TagList = React.memo( setIsEditTags(false); } }, [form, onSubmit]); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); return ( @@ -75,7 +93,7 @@ export const TagList = React.memo( )} - + {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} {tags.length > 0 && !isEditTags && @@ -98,9 +116,30 @@ export const TagList = React.memo( euiFieldProps: { fullWidth: true, placeholder: '', + options, + noSuggestions: false, }, }} /> + + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} +
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx index 1c71260422d4b..ff402e8ea1c8b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx @@ -8,17 +8,13 @@ import React from 'react'; import { mount } from 'enzyme'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { getFormMock } from '../__mock__/form'; +import { getFormMock, useFormMock } from '../__mock__/form'; import { useUpdateComment } from '../../../../containers/case/use_update_comment'; import { basicCase, getUserAction } from '../../../../containers/case/mock'; import { UserActionTree } from './'; import { TestProviders } from '../../../../mock'; -import { useFormMock } from '../create/index.test'; import { wait } from '../../../../lib/helpers'; import { act } from 'react-dom/test-utils'; -jest.mock( - '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); const fetchUserActions = jest.fn(); const onUpdateField = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 8bea504f84206..97c89f91c12bd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -243,7 +243,7 @@ export const getMonitoringColumns = (): RulesStatusesColumns[] => { {value != null && value.length > 0 ? Math.max(...value?.map(item => Number.parseFloat(item))) - : null} + : getEmptyTagValue()} ), truncateText: true, @@ -256,7 +256,7 @@ export const getMonitoringColumns = (): RulesStatusesColumns[] => { {value != null && value.length > 0 ? Math.max(...value?.map(item => Number.parseFloat(item))) - : null} + : getEmptyTagValue()} ), truncateText: true, @@ -267,7 +267,7 @@ export const getMonitoringColumns = (): RulesStatusesColumns[] => { name: i18n.COLUMN_GAP, render: (value: RuleStatus['current_status']['gap']) => ( - {value} + {value ?? getEmptyTagValue()} ), truncateText: true, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index 5b7a85e23834d..d3014354ab7b3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -41,7 +41,7 @@ const EuiBadgeWrap = styled(EuiBadge)` .euiBadge__text { white-space: pre-wrap !important; } -`; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any export const buildQueryBarDescription = ({ field, diff --git a/x-pack/legacy/plugins/siem/public/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts index c83433ef129c9..0c0ac637a4229 100644 --- a/x-pack/legacy/plugins/siem/public/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/shared_imports.ts @@ -8,6 +8,7 @@ export { getUseField, getFieldValidityAndErrorMessage, FieldHook, + FieldValidateResponse, FIELD_TYPES, Form, FormData, @@ -17,6 +18,7 @@ export { UseField, useForm, ValidationFunc, + VALIDATION_TYPES, } from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { Field, diff --git a/x-pack/legacy/plugins/task_manager/server/index.ts b/x-pack/legacy/plugins/task_manager/server/index.ts index ff25d8a1e0e5d..3ea687f7003f4 100644 --- a/x-pack/legacy/plugins/task_manager/server/index.ts +++ b/x-pack/legacy/plugins/task_manager/server/index.ts @@ -15,19 +15,26 @@ export { LegacyTaskManagerApi, getTaskManagerSetup, getTaskManagerStart } from ' // Once all plugins are migrated to NP, this can be removed // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TaskManager } from '../../../../plugins/task_manager/server/task_manager'; +import { + LegacyPluginApi, + LegacyPluginSpec, + ArrayOrItem, +} from '../../../../../src/legacy/plugin_discovery/types'; const savedObjectSchemas = { task: { hidden: true, isNamespaceAgnostic: true, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + // legacy config is marked as any in core, no choice here + // eslint-disable-next-line @typescript-eslint/no-explicit-any indexPattern(config: any) { return config.get('xpack.task_manager.index'); }, }, }; -export function taskManager(kibana: any) { +export function taskManager(kibana: LegacyPluginApi): ArrayOrItem { return new kibana.Plugin({ id: 'task_manager', require: ['kibana', 'elasticsearch', 'xpack_main'], @@ -58,7 +65,11 @@ export function taskManager(kibana: any) { // instead we will start the internal Task Manager plugin when // all legacy plugins have finished initializing // Once all plugins are migrated to NP, this can be removed - this.kbnServer.afterPluginsInit(() => { + + // the typing for the lagcy server isn't quite correct, so + // we'll bypase it for now + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this as any).kbnServer.afterPluginsInit(() => { taskManagerPlugin.start(); }); return taskManagerPlugin; @@ -71,5 +82,5 @@ export function taskManager(kibana: any) { migrations, savedObjectSchemas, }, - }); + } as Legacy.PluginSpecOptions); } diff --git a/x-pack/legacy/plugins/task_manager/server/legacy.ts b/x-pack/legacy/plugins/task_manager/server/legacy.ts index cd2047b757e61..0d50828004a94 100644 --- a/x-pack/legacy/plugins/task_manager/server/legacy.ts +++ b/x-pack/legacy/plugins/task_manager/server/legacy.ts @@ -49,10 +49,10 @@ export function createLegacyApi(legacyTaskManager: Promise): Legacy fetch: (opts: SearchOpts) => legacyTaskManager.then((tm: TaskManager) => tm.fetch(opts)), get: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.get(id)), remove: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.remove(id)), - schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: any) => + schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: object) => legacyTaskManager.then((tm: TaskManager) => tm.schedule(taskInstance, options)), runNow: (taskId: string) => legacyTaskManager.then((tm: TaskManager) => tm.runNow(taskId)), - ensureScheduled: (taskInstance: TaskInstanceWithId, options?: any) => + ensureScheduled: (taskInstance: TaskInstanceWithId, options?: object) => legacyTaskManager.then((tm: TaskManager) => tm.ensureScheduled(taskInstance, options)), }; } diff --git a/x-pack/legacy/plugins/task_manager/server/migrations.ts b/x-pack/legacy/plugins/task_manager/server/migrations.ts index 97c4f97f59c58..1c2cf73d0fe13 100644 --- a/x-pack/legacy/plugins/task_manager/server/migrations.ts +++ b/x-pack/legacy/plugins/task_manager/server/migrations.ts @@ -7,7 +7,7 @@ import { SavedObject } from '../../../../../src/core/server'; export const migrations = { task: { - '7.4.0': (doc: SavedObject>) => ({ + '7.4.0': (doc: SavedObject>) => ({ ...doc, updated_at: new Date().toISOString(), }), @@ -18,7 +18,7 @@ export const migrations = { function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, ...doc -}: SavedObject>) { +}: SavedObject>) { return { ...doc, attributes: { diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts index 74783cf46550f..72d498056d6b3 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/index.ts @@ -9,6 +9,7 @@ export { CHART_FORMAT_LIMITS } from './chart_format_limits'; export { CLIENT_DEFAULTS } from './client_defaults'; export { CONTEXT_DEFAULTS } from './context_defaults'; export * from './capabilities'; +export * from './settings_defaults'; export { PLUGIN } from './plugin'; export { QUERY, STATES } from './query'; export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts new file mode 100644 index 0000000000000..b7986679a09ca --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts @@ -0,0 +1,15 @@ +/* + * 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 { DynamicSettings } from '../runtime_types'; + +export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = { + heartbeatIndices: 'heartbeat-8*', + certThresholds: { + expiration: 30, + age: 365, + }, +}; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts index 985b51891da99..da887cc5055c1 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -6,19 +6,15 @@ import * as t from 'io-ts'; -export const CertificatesStatesThresholdType = t.interface({ - warningState: t.number, - errorState: t.number, +export const CertStateThresholdsType = t.type({ + age: t.number, + expiration: t.number, }); -export const DynamicSettingsType = t.intersection([ - t.type({ - heartbeatIndices: t.string, - }), - t.partial({ - certificatesThresholds: CertificatesStatesThresholdType, - }), -]); +export const DynamicSettingsType = t.type({ + heartbeatIndices: t.string, + certThresholds: CertStateThresholdsType, +}); export const DynamicSettingsSaveType = t.intersection([ t.type({ @@ -31,12 +27,4 @@ export const DynamicSettingsSaveType = t.intersection([ export type DynamicSettings = t.TypeOf; export type DynamicSettingsSaveResponse = t.TypeOf; -export type CertificatesStatesThreshold = t.TypeOf; - -export const defaultDynamicSettings: DynamicSettings = { - heartbeatIndices: 'heartbeat-8*', - certificatesThresholds: { - errorState: 7, - warningState: 30, - }, -}; +export type CertStateThresholds = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx index acfe2ada5b68d..6328789d03f29 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { EmptyStateComponent } from '../empty_state'; import { StatesIndexStatus } from '../../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http'; -import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError, IHttpFetchError } from 'src/core/public'; import { mountWithRouter, shallowWithRouter } from '../../../../lib'; describe('EmptyState component', () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap index 36bc9bb860211..96d472c91680d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap @@ -52,17 +52,18 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] = } > diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap index 93151198c0f49..3b0c6d99fd9f8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap @@ -52,17 +52,18 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] = } > diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx index a3158f3d72445..3d4bd58aabe0f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx @@ -13,12 +13,13 @@ describe('CertificateForm', () => { expect( shallowWithRouter( ) diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx index 654d51019d4e5..07a3bf81e39d8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx @@ -13,12 +13,13 @@ describe('CertificateForm', () => { expect( shallowWithRouter( ) diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx index 5103caee1e1c0..209e38785e165 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx @@ -6,155 +6,157 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useSelector } from 'react-redux'; import { EuiDescribedFormGroup, EuiFormRow, EuiCode, EuiFieldNumber, + EuiText, EuiTitle, EuiSpacer, - EuiSelect, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import { defaultDynamicSettings, DynamicSettings } from '../../../common/runtime_types'; -import { selectDynamicSettings } from '../../state/selectors'; +import { CertStateThresholds } from '../../../common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { SettingsFormProps } from '../../pages/settings'; -type NumStr = string | number; - -export type OnFieldChangeType = (field: string, value?: NumStr) => void; - -export interface SettingsFormProps { - onChange: OnFieldChangeType; - formFields: DynamicSettings | null; - fieldErrors: any; - isDisabled: boolean; +interface ChangedValues { + heartbeatIndices?: string; + certThresholds?: Partial; } +export type OnFieldChangeType = (changedValues: ChangedValues) => void; + export const CertificateExpirationForm: React.FC = ({ + loading, onChange, formFields, fieldErrors, isDisabled, -}) => { - const dss = useSelector(selectDynamicSettings); - - return ( - <> - -

+}) => ( + <> + +

+ +

+
+ + +

+ } + description={ + + } + > + {DYNAMIC_SETTINGS_DEFAULTS.certThresholds.expiration} + ), + }} /> - -
- - - - } - description={ + isInvalid={!!fieldErrors?.certificatesThresholds?.expirationThresholdError} + label={ } > - {defaultDynamicSettings?.certificatesThresholds?.errorState} - ), - }} - /> - } - isInvalid={!!fieldErrors?.certificatesThresholds?.errorState} - label={ - + + + onChange({ + certThresholds: { + expiration: Number(e.target.value), + }, + }) + } /> - } - > - - - - onChange( - 'certificatesThresholds.errorState', - value === '' ? undefined : Number(value) - ) - } + + + + - - - - - - - {defaultDynamicSettings?.certificatesThresholds?.warningState} - ), - }} - /> - } - isInvalid={!!fieldErrors?.certificatesThresholds?.warningState} - label={ - +
+ + + {DYNAMIC_SETTINGS_DEFAULTS.certThresholds.age}, + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.ageThresholdError} + label={ + + } + > + + + + onChange({ + certThresholds: { age: Number(e.currentTarget.value) }, + }) + } /> - } - > - - - - onChange('certificatesThresholds.warningState', Number(event.currentTarget.value)) - } + + + + - - - - - - - - - ); -}; + + + + + + +); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx index c28eca2ea229e..b9a5ca0e730de 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useSelector } from 'react-redux'; import { EuiDescribedFormGroup, EuiFormRow, @@ -15,76 +14,72 @@ import { EuiTitle, EuiSpacer, } from '@elastic/eui'; -import { defaultDynamicSettings } from '../../../common/runtime_types'; -import { selectDynamicSettings } from '../../state/selectors'; -import { SettingsFormProps } from './certificate_form'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { SettingsFormProps } from '../../pages/settings'; export const IndicesForm: React.FC = ({ onChange, + loading, formFields, fieldErrors, isDisabled, -}) => { - const dss = useSelector(selectDynamicSettings); - - return ( - <> - -

+}) => ( + <> + +

+ +

+
+ + + +

+ } + description={ + + } + > + {DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices}, + }} /> - -
- - - - } - description={ + isInvalid={!!fieldErrors?.heartbeatIndices} + label={ } > - {defaultDynamicSettings.heartbeatIndices}, - }} - /> - } - isInvalid={!!fieldErrors?.heartbeatIndices} - label={ - - } - > - onChange('heartbeatIndices', event.currentTarget.value)} - /> - - - - ); -}; + disabled={isDisabled} + isLoading={loading} + value={formFields?.heartbeatIndices || ''} + onChange={(event: any) => onChange({ heartbeatIndices: event.currentTarget.value })} + /> + + + +); diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx index 6defb96e0da3d..d8c2a78092854 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx @@ -17,8 +17,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch, useSelector } from 'react-redux'; -import { cloneDeep, isEqual, set } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; import { Link } from 'react-router-dom'; import { selectDynamicSettings } from '../state/selectors'; import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; @@ -32,21 +31,38 @@ import { CertificateExpirationForm, OnFieldChangeType, } from '../components/settings/certificate_form'; - -const getFieldErrors = (formFields: DynamicSettings | null) => { +import * as Translations from './translations'; + +interface SettingsPageFieldErrors { + heartbeatIndices: 'May not be blank' | ''; + certificatesThresholds: { + expirationThresholdError: string | null; + ageThresholdError: string | null; + } | null; +} + +export interface SettingsFormProps { + loading: boolean; + onChange: OnFieldChangeType; + formFields: DynamicSettings | null; + fieldErrors: SettingsPageFieldErrors | null; + isDisabled: boolean; +} + +const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldErrors | null => { if (formFields) { const blankStr = 'May not be blank'; - const { certificatesThresholds, heartbeatIndices } = formFields; + const { certThresholds: certificatesThresholds, heartbeatIndices } = formFields; const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; - const errorStateErr = certificatesThresholds?.errorState ? null : blankStr; - const warningStateErr = certificatesThresholds?.warningState ? null : blankStr; + const expirationThresholdError = certificatesThresholds?.expiration ? null : blankStr; + const ageThresholdError = certificatesThresholds?.age ? null : blankStr; return { heartbeatIndices: heartbeatIndErr, certificatesThresholds: - errorStateErr || warningStateErr + expirationThresholdError || ageThresholdError ? { - errorState: errorStateErr, - warningState: warningStateErr, + expirationThresholdError, + ageThresholdError, } : null, }; @@ -57,10 +73,7 @@ const getFieldErrors = (formFields: DynamicSettings | null) => { export const SettingsPage = () => { const dss = useSelector(selectDynamicSettings); - const settingsBreadcrumbText = i18n.translate('xpack.uptime.settingsBreadcrumbText', { - defaultMessage: 'Settings', - }); - useBreadcrumbs([{ text: settingsBreadcrumbText }]); + useBreadcrumbs([{ text: Translations.settings.breadcrumbText }]); useUptimeTelemetry(UptimePage.Settings); @@ -70,21 +83,28 @@ export const SettingsPage = () => { dispatch(getDynamicSettings()); }, [dispatch]); - const [formFields, setFormFields] = useState(dss.settings || null); + const [formFields, setFormFields] = useState( + dss.settings ? { ...dss.settings } : null + ); - if (!dss.loadError && formFields == null && dss.settings) { - setFormFields({ ...dss.settings }); + if (!dss.loadError && formFields === null && dss.settings) { + setFormFields(Object.assign({}, { ...dss.settings })); } const fieldErrors = getFieldErrors(formFields); const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); - const onChangeFormField: OnFieldChangeType = (field, value) => { + const onChangeFormField: OnFieldChangeType = changedField => { if (formFields) { - const newFormFields = cloneDeep(formFields); - set(newFormFields, field, value); - setFormFields(cloneDeep(newFormFields)); + setFormFields({ + heartbeatIndices: changedField.heartbeatIndices ?? formFields.heartbeatIndices, + certThresholds: Object.assign( + {}, + formFields.certThresholds, + changedField?.certThresholds ?? null + ), + }); } }; @@ -95,27 +115,18 @@ export const SettingsPage = () => { } }; - const resetForm = () => { - if (formFields && dss.settings) { - setFormFields({ ...dss.settings }); - } - }; + const resetForm = () => setFormFields(dss.settings ? { ...dss.settings } : null); - const isFormDirty = dss.settings ? !isEqual(dss.settings, formFields) : true; + const isFormDirty = !isEqual(dss.settings, formFields); const canEdit: boolean = !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; const isFormDisabled = dss.loading || !canEdit; - const editNoticeTitle = i18n.translate('xpack.uptime.settings.cannotEditTitle', { - defaultMessage: 'You do not have permission to edit settings.', - }); - const editNoticeText = i18n.translate('xpack.uptime.settings.cannotEditText', { - defaultMessage: - "Your user currently has 'Read' permissions for the Uptime app. Enable a permissions-level of 'All' to edit these settings.", - }); const cannotEditNotice = canEdit ? null : ( <> - {editNoticeText} + + {Translations.settings.editNoticeText} + ); @@ -124,9 +135,7 @@ export const SettingsPage = () => { <> - {i18n.translate('xpack.uptime.settings.returnToOverviewLinkLabel', { - defaultMessage: 'Return to overview', - })} + {Translations.settings.returnToOverviewLinkLabel} @@ -139,12 +148,14 @@ export const SettingsPage = () => {
('CREATE_ML_JOB'); -export const getMLCapabilitiesAction = createAsyncAction( +export const getMLCapabilitiesAction = createAsyncAction( 'GET_ML_CAPABILITIES' ); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts index 66b376c3ac36f..ff9fcd0573257 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -6,7 +6,7 @@ import { fetchSnapshotCount } from '../snapshot'; import { apiService } from '../utils'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; describe('snapshot API', () => { let fetchMock: jest.SpyInstance>; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts index f10745a50f56a..16b90e9921428 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts @@ -8,7 +8,7 @@ import moment from 'moment'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults, @@ -20,7 +20,7 @@ import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_r export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase(); -export const getMLCapabilities = async (): Promise => { +export const getMLCapabilities = async (): Promise => { return await apiService.get(API_URLS.ML_CAPABILITIES); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts index 0354cfeac7b07..4ec35d8cd6c6f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts @@ -7,7 +7,7 @@ import { call, put } from 'redux-saga/effects'; import { fetchEffectFactory } from '../fetch_effect'; import { indexStatusAction } from '../../actions'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; import { StatesIndexStatus } from '../../../../common/runtime_types'; import { fetchIndexStatus } from '../../api'; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts index f003565e9873e..a9ad58e64e552 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts @@ -31,23 +31,19 @@ export const dynamicSettingsReducer = handleActions( ...state, loading: true, }), - [String(getDynamicSettingsSuccess)]: (state, action: Action) => { - return { - loading: false, - settings: action.payload, - }; - }, - [String(getDynamicSettingsFail)]: (state, action: Action) => { - return { - loading: false, - loadError: action.payload, - }; - }, + [String(getDynamicSettingsSuccess)]: (_state, action: Action) => ({ + loading: false, + settings: action.payload, + }), + [String(getDynamicSettingsFail)]: (_state, action: Action) => ({ + loading: false, + loadError: action.payload, + }), [String(setDynamicSettings)]: state => ({ ...state, loading: true, }), - [String(setDynamicSettingsSuccess)]: (state, action: Action) => ({ + [String(setDynamicSettingsSuccess)]: (_state, action: Action) => ({ settings: action.payload, saveSucceded: true, loading: false, diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts index 9d59d39634327..df5e825c3488b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts @@ -17,7 +17,7 @@ import { import { getAsyncInitialState, handleAsyncAction } from './utils'; import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; import { AsyncInitialState } from './types'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; @@ -26,7 +26,7 @@ export interface MLJobState { createJob: AsyncInitialState; deleteJob: AsyncInitialState; anomalies: AsyncInitialState; - mlCapabilities: AsyncInitialState; + mlCapabilities: AsyncInitialState; } const initialState: MLJobState = { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts index 2b7c04178e9b4..ba5e5abf588b8 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -6,6 +6,7 @@ import { getBasePath, isIntegrationsPopupOpen } from '../index'; import { AppState } from '../../../state'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('state selectors', () => { const state: AppState = { @@ -20,6 +21,7 @@ describe('state selectors', () => { loading: false, }, dynamicSettings: { + settings: DYNAMIC_SETTINGS_DEFAULTS, loading: false, }, monitor: { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 7260c61f44147..15fc8b8a7b173 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -24,9 +24,7 @@ export const monitorLocationsSelector = (state: AppState, monitorId: string) => export const monitorStatusSelector = (state: AppState) => state.monitorStatus.status; -export const selectDynamicSettings = (state: AppState) => { - return state.dynamicSettings; -}; +export const selectDynamicSettings = (state: AppState) => state.dynamicSettings; export const selectIndexPattern = ({ indexPattern }: AppState) => { return { indexPattern: indexPattern.index_pattern, loading: indexPattern.loading }; diff --git a/x-pack/package.json b/x-pack/package.json index 3c6146b491f60..d0025ff436649 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -91,6 +91,7 @@ "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", "@types/react": "^16.9.19", + "@types/react-beautiful-dnd": "^12.1.1", "@types/react-dom": "^16.9.5", "@types/react-redux": "^7.1.7", "@types/react-router-dom": "^5.1.3", @@ -182,9 +183,9 @@ "@elastic/apm-rum-react": "^1.1.1", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.2.0", + "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.1.1", "@elastic/numeral": "2.4.0", "@kbn/babel-preset": "1.0.0", @@ -249,7 +250,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", - "handlebars": "4.5.3", + "handlebars": "4.7.6", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", @@ -305,7 +306,7 @@ "re-resizable": "^6.1.1", "react": "^16.12.0", "react-apollo": "^2.1.4", - "react-beautiful-dnd": "^8.0.7", + "react-beautiful-dnd": "^12.2.0", "react-datetime": "^2.14.0", "react-dom": "^16.12.0", "react-dropzone": "^4.2.9", diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 82cc09f5e9eca..d6c85606edc2c 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -263,7 +263,7 @@ Kibana ships with a set of built-in action types: | Type | Id | Description | | ------------------------- | ------------- | ------------------------------------------------------------------ | -| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` | +| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | | [Email](#email) | `.email` | Sends an email using SMTP | | [Slack](#slack) | `.slack` | Posts a message to a slack channel | | [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 61b338d47b9f5..49e8f3e80b14a 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -19,6 +19,8 @@ export interface ActionResult { id: string; actionTypeId: string; name: string; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record; isPreconfigured: boolean; } diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index c1d979feacc1d..735ec349837a9 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -81,7 +81,7 @@ export class ActionTypeRegistry { title: actionType.name, type: `actions:${actionType.id}`, maxAttempts: actionType.maxAttempts || 1, - getRetry(attempts: number, error: any) { + getRetry(attempts: number, error: unknown) { if (error instanceof ExecutorError) { return error.retry == null ? false : error.retry; } diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 431bfb1e99c3b..64b43e1ab6bbc 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -7,9 +7,10 @@ import { ActionsClient } from './actions_client'; type ActionsClientContract = PublicMethodsOf; +export type ActionsClientMock = jest.Mocked; const createActionsClientMock = () => { - const mocked: jest.Mocked = { + const mocked: ActionsClientMock = { create: jest.fn(), get: jest.fn(), delete: jest.fn(), @@ -19,6 +20,8 @@ const createActionsClientMock = () => { return mocked; }; -export const actionsClientMock = { +export const actionsClientMock: { + create: () => ActionsClientMock; +} = { create: createActionsClientMock, }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 955e1569380a5..14441bfd52dd7 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -348,9 +348,6 @@ describe('get()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, }); expect(savedObjectsClient.get).not.toHaveBeenCalled(); }); @@ -418,9 +415,6 @@ describe('getAll()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, referencedByCount: 2, }, ]); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 8f73bfb31ea4d..618bc8a85e856 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -135,7 +135,7 @@ export class ActionsClient { id, actionTypeId: result.attributes.actionTypeId as string, name: result.attributes.name as string, - config: result.attributes.config as Record, + config: result.attributes.config as Record, isPreconfigured: false, }; } @@ -152,7 +152,6 @@ export class ActionsClient { id, actionTypeId: preconfiguredActionsList.actionTypeId, name: preconfiguredActionsList.name, - config: preconfiguredActionsList.config, isPreconfigured: true, }; } @@ -184,7 +183,6 @@ export class ActionsClient { id: preconfiguredAction.id, actionTypeId: preconfiguredAction.actionTypeId, name: preconfiguredAction.name, - config: preconfiguredAction.config, isPreconfigured: true, })), ].sort((a, b) => a.name.localeCompare(b.name)); @@ -230,7 +228,7 @@ async function injectExtraFindData( scopedClusterClient: IScopedClusterClient, actionResults: ActionResult[] ): Promise { - const aggs: Record = {}; + const aggs: Record = {}; for (const actionResult of actionResults) { aggs[actionResult.id] = { filter: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 658f8f3fd8cf9..265ff267f222e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -30,7 +30,7 @@ const NO_OP_FN = () => {}; const services = { log: NO_OP_FN, - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; @@ -52,7 +52,7 @@ describe('actionTypeRegistry.get() works', () => { describe('config validation', () => { test('config validation succeeds when config is valid', () => { - const config: Record = { + const config: Record = { service: 'gmail', from: 'bob@example.com', }; @@ -74,7 +74,7 @@ describe('config validation', () => { }); test('config validation fails when config is not valid', () => { - const baseConfig: Record = { + const baseConfig: Record = { from: 'bob@example.com', }; @@ -177,7 +177,7 @@ describe('config validation', () => { describe('secrets validation', () => { test('secrets validation succeeds when secrets is valid', () => { - const secrets: Record = { + const secrets: Record = { user: 'bob', password: 'supersecret', }; @@ -185,7 +185,7 @@ describe('secrets validation', () => { }); test('secrets validation succeeds when secrets props are null/undefined', () => { - const secrets: Record = { + const secrets: Record = { user: null, password: null, }; @@ -197,7 +197,7 @@ describe('secrets validation', () => { describe('params validation', () => { test('params validation succeeds when params is valid', () => { - const params: Record = { + const params: Record = { to: ['bob@example.com'], subject: 'this is a test', message: 'this is the message', diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index ca8d089ad2946..7ddb123a4d780 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -30,10 +30,10 @@ const ConfigSchema = schema.object(ConfigSchemaProps); function validateConfig( configurationUtilities: ActionsConfigurationUtilities, - configObject: any + configObject: unknown ): string | void { // avoids circular reference ... - const config: ActionTypeConfigType = configObject; + const config = configObject as ActionTypeConfigType; // Make sure service is set, or if not, both host/port must be set. // If service is set, host/port are ignored, when the email is sent. @@ -95,9 +95,9 @@ const ParamsSchema = schema.object( } ); -function validateParams(paramsObject: any): string | void { +function validateParams(paramsObject: unknown): string | void { // avoids circular reference ... - const params: ActionParamsType = paramsObject; + const params = paramsObject as ActionParamsType; const { to, cc, bcc } = params; const addrs = to.length + cc.length + bcc.length; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index ec495aed7675a..ed57e44c3f0b3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -43,7 +43,7 @@ describe('actionTypeRegistry.get() works', () => { describe('config validation', () => { test('config validation succeeds when config is valid', () => { - const config: Record = { + const config: Record = { index: 'testing-123', refresh: false, }; @@ -97,7 +97,7 @@ describe('config validation', () => { }); test('config validation fails when config is not valid', () => { - const baseConfig: Record = { + const baseConfig: Record = { indeX: 'bob', }; @@ -111,7 +111,7 @@ describe('config validation', () => { describe('params validation', () => { test('params validation succeeds when params is valid', () => { - const params: Record = { + const params: Record = { documents: [{ rando: 'thing' }], }; expect(validateParams(actionType, params)).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index ff7b27b3f51fc..32f5e23015700 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -72,13 +72,12 @@ async function executor( bulkBody.push(document); } - const bulkParams: any = { + const bulkParams: unknown = { index, body: bulkBody, + refresh: config.refresh, }; - bulkParams.refresh = config.refresh; - let result; try { result = await services.callCluster('bulk', bulkParams); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts index cc9d36ff86342..92f88ebe0be22 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts @@ -9,7 +9,7 @@ import { Services } from '../../types'; interface PostPagerdutyOptions { apiUrl: string; - data: any; + data: unknown; headers: Record; services: Services; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 42160dc2fc22b..a02f79a49e8e2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -63,12 +63,15 @@ describe('send_email module', () => { }); test('handles unauthenticated email using not secure host/port', async () => { - const sendEmailOptions = getSendEmailOptions(); + const sendEmailOptions = getSendEmailOptions({ + transport: { + host: 'example.com', + port: 1025, + }, + }); delete sendEmailOptions.transport.service; delete sendEmailOptions.transport.user; delete sendEmailOptions.transport.password; - sendEmailOptions.transport.host = 'example.com'; - sendEmailOptions.transport.port = 1025; const result = await sendEmail(mockLogger, sendEmailOptions); expect(result).toBe(sendMailMockResult); expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -105,13 +108,17 @@ describe('send_email module', () => { }); test('handles unauthenticated email using secure host/port', async () => { - const sendEmailOptions = getSendEmailOptions(); + const sendEmailOptions = getSendEmailOptions({ + transport: { + host: 'example.com', + port: 1025, + secure: true, + }, + }); delete sendEmailOptions.transport.service; delete sendEmailOptions.transport.user; delete sendEmailOptions.transport.password; - sendEmailOptions.transport.host = 'example.com'; - sendEmailOptions.transport.port = 1025; - sendEmailOptions.transport.secure = true; + const result = await sendEmail(mockLogger, sendEmailOptions); expect(result).toBe(sendMailMockResult); expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -154,19 +161,22 @@ describe('send_email module', () => { }); }); -function getSendEmailOptions(): any { +function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}) { return { content: { + ...content, message: 'a message', subject: 'a subject', }, routing: { + ...routing, from: 'fred@example.com', to: ['jim@example.com'], cc: ['bob@example.com', 'robert@example.com'], bcc: [], }, transport: { + ...transport, service: 'whatever', user: 'elastic', password: 'changeme', diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index ffbf7485a8b0b..869db34f034ae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -43,13 +43,13 @@ export interface Content { } // send an email -export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { +export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { const { transport, routing, content } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; - const transportConfig: Record = {}; + const transportConfig: Record = {}; if (user != null && password != null) { transportConfig.auth = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index 514c9759d7b56..e54c8179ae7b4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -22,7 +22,7 @@ const postPagerdutyMock = postPagerduty as jest.Mock; const ACTION_TYPE_ID = '.pagerduty'; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; @@ -142,6 +142,25 @@ describe('validateParams()', () => { - [eventAction.2]: expected value to equal [acknowledge]" `); }); + + test('should validate and throw error when timestamp has spaces', () => { + const randoDate = new Date('1963-09-23T01:23:45Z').toISOString(); + const timestamp = ` ${randoDate}`; + expect(() => { + validateParams(actionType, { + timestamp, + }); + }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); + }); + + test('should validate and throw error when timestamp is invalid', () => { + const timestamp = `1963-09-55 90:23:45`; + expect(() => { + validateParams(actionType, { + timestamp, + }); + }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); + }); }); describe('execute()', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 2b607d0dd41ba..0c8802060164d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -68,20 +68,27 @@ const ParamsSchema = schema.object( { validate: validateParams } ); -function validateParams(paramsObject: any): string | void { - const params: ActionParamsType = paramsObject; - - const { timestamp } = params; +function validateParams(paramsObject: unknown): string | void { + const { timestamp } = paramsObject as ActionParamsType; if (timestamp != null) { - let date; try { - date = Date.parse(timestamp); + const date = Date.parse(timestamp); + if (isNaN(date)) { + return i18n.translate('xpack.actions.builtin.pagerduty.invalidTimestampErrorMessage', { + defaultMessage: `error parsing timestamp "{timestamp}"`, + values: { + timestamp, + }, + }); + } } catch (err) { - return 'error parsing timestamp: ${err.message}'; - } - - if (isNaN(date)) { - return 'error parsing timestamp'; + return i18n.translate('xpack.actions.builtin.pagerduty.timestampParsingFailedErrorMessage', { + defaultMessage: `error parsing timestamp "{timestamp}": {message}`, + values: { + timestamp, + message: err.message, + }, + }); } } } @@ -210,11 +217,23 @@ async function executor( const AcknowledgeOrResolve = new Set([EVENT_ACTION_ACKNOWLEDGE, EVENT_ACTION_RESOLVE]); -function getBodyForEventAction(actionId: string, params: ActionParamsType): any { +function getBodyForEventAction(actionId: string, params: ActionParamsType): unknown { const eventAction = params.eventAction || EVENT_ACTION_TRIGGER; const dedupKey = params.dedupKey || `action:${actionId}`; - const data: any = { + const data: { + event_action: ActionParamsType['eventAction']; + dedup_key: string; + payload?: { + summary: string; + source: string; + severity: string; + timestamp?: string; + component?: string; + group?: string; + class?: string; + }; + } = { event_action: eventAction, dedup_key: dedupKey, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index bb806f8ae36fc..3ce01c59596f6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -91,7 +91,7 @@ describe('execute()', () => { await actionType.executor({ actionId, services: { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }, params: { message: 'message text here', level: 'info' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index fb296089e9ec5..9166f53cf757e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -57,14 +57,14 @@ export const handleCreateIncident = async ({ comments && Array.isArray(comments) && comments.length > 0 && - mapping.get('comments').actionType !== 'nothing' + mapping.get('comments')?.actionType !== 'nothing' ) { comments = transformComments(comments, params, ['informationAdded']); res.comments = [ ...(await createComments( serviceNow, res.incidentId, - mapping.get('comments').target, + mapping.get('comments')!.target, comments )), ]; @@ -103,11 +103,11 @@ export const handleUpdateIncident = async ({ comments && Array.isArray(comments) && comments.length > 0 && - mapping.get('comments').actionType !== 'nothing' + mapping.get('comments')?.actionType !== 'nothing' ) { comments = transformComments(comments, params, ['informationAdded']); res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ...(await createComments(serviceNow, incidentId, mapping.get('comments')!.target, comments)), ]; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 750fda93b60d6..0a26996ea8d69 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -39,7 +39,7 @@ export const buildMap = (mapping: MapEntry[]): Mapping => { }, new Map()); }; -export const mapParams = (params: any, mapping: Mapping) => { +export const mapParams = (params: Record, mapping: Mapping) => { return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { const field = mapping.get(curr); if (field) { @@ -61,11 +61,11 @@ export const prepareFieldsForTransformation = ({ defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { return Object.keys(params.incident) - .filter(p => mapping.get(p).actionType !== 'nothing') + .filter(p => mapping.get(p)!.actionType !== 'nothing') .map(p => ({ key: p, - value: params.incident[p], - actionType: mapping.get(p).actionType, + value: params.incident[p] as string, + actionType: mapping.get(p)!.actionType, pipes: [...defaultPipes], })) .map(p => ({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 1a23354e6490d..08b837cf8d0a5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -22,7 +22,7 @@ jest.mock('./action_handlers'); const handleIncidentMock = handleIncident as jest.Mock; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index a63c2fd3a6ceb..5066190d4fe56 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -85,7 +85,7 @@ async function serviceNowExecutor( const { comments, incidentId, ...restParams } = params; const mapping = buildMap(configurationMapping); - const incident = mapParams(restParams, mapping); + const incident = mapParams((restParams as unknown) as Record, mapping); const serviceNow = new ServiceNow({ url: apiUrl, username, password }); const handlerInput = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index cc07a0b90330d..ed9cfe67a19a1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -49,14 +49,14 @@ class ServiceNow { }: { url: string; method?: Method; - data?: any; + data?: unknown; }): Promise { const res = await this.axios(url, { method, data }); this._throwIfNotAlive(res.status, res.headers['content-type']); return res; } - private _patch({ url, data }: { url: string; data: any }): Promise { + private _patch({ url, data }: { url: string; data: unknown }): Promise { return this._request({ url, method: 'patch', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 71b05be8f3e4d..c5ef282aeffa7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -30,10 +30,10 @@ export type CasesConfigurationType = TypeOf; export type MapEntry = TypeOf; export type Comment = TypeOf; -export type Mapping = Map; +export type Mapping = Map>; export interface Params extends ExecutorParams { - incident: Record; + incident: Record; } export interface CreateHandlerArguments { serviceNow: ServiceNow; @@ -66,7 +66,7 @@ export interface AppendFieldArgs { } export interface KeyAny { - [index: string]: string; + [index: string]: unknown; } export interface AppendInformationFieldArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 49b0b84e9dbb5..22c4b63474fdc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; +import { + ActionType, + Services, + ActionTypeExecutorOptions, + ActionTypeExecutorResult, +} from '../types'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { validateParams, validateSecrets } from '../lib'; import { getActionType } from './slack'; @@ -13,7 +18,7 @@ import { actionsConfigMock } from '../actions_config.mock'; const ACTION_TYPE_ID = '.slack'; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; @@ -21,7 +26,7 @@ let actionType: ActionType; beforeAll(() => { actionType = getActionType({ - async executor(options: ActionTypeExecutorOptions): Promise {}, + async executor() {}, configurationUtilities: actionsConfigMock.create(), }); }); @@ -117,7 +122,7 @@ describe('validateActionTypeSecrets()', () => { describe('execute()', () => { beforeAll(() => { - async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise { + async function mockSlackExecutor(options: ActionTypeExecutorOptions) { const { params } = options; const { message } = params; if (message == null) throw new Error('message property required in parameter'); @@ -130,7 +135,9 @@ describe('execute()', () => { return { text: `slack mockExecutor success: ${message}`, - }; + actionId: '', + status: 'ok', + } as ActionTypeExecutorResult; } actionType = getActionType({ @@ -148,10 +155,12 @@ describe('execute()', () => { params: { message: 'this invocation should succeed' }, }); expect(response).toMatchInlineSnapshot(` -Object { - "text": "slack mockExecutor success: this invocation should succeed", -} -`); + Object { + "actionId": "", + "status": "ok", + "text": "slack mockExecutor success: this invocation should succeed", + } + `); }); test('calls the mock executor with failure', async () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index e51ef3f67bd65..edf3e485f3c57 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -156,7 +156,7 @@ async function slackExecutor( return successResult(actionId, result); } -function successResult(actionId: string, data: any): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 03658b3b1dd85..1beaf92f3f48b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -22,7 +22,7 @@ const axiosRequestMock = axios.request as jest.Mock; const ACTION_TYPE_ID = '.webhook'; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; @@ -44,7 +44,7 @@ describe('actionType', () => { describe('secrets validation', () => { test('succeeds when secrets is valid', () => { - const secrets: Record = { + const secrets: Record = { user: 'bob', password: 'supersecret', }; @@ -60,20 +60,18 @@ describe('secrets validation', () => { }); test('succeeds when basic authentication credentials are omitted', () => { - expect(() => { - validateSecrets(actionType, {}).toEqual({}); - }); + expect(validateSecrets(actionType, {})).toEqual({ password: null, user: null }); }); }); describe('config validation', () => { - const defaultValues: Record = { + const defaultValues: Record = { headers: null, method: 'post', }; test('config validation passes when only required fields are provided', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', }; expect(validateConfig(actionType, config)).toEqual({ @@ -84,7 +82,7 @@ describe('config validation', () => { test('config validation passes when valid methods are provided', () => { ['post', 'put'].forEach(method => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', method, }; @@ -96,7 +94,7 @@ describe('config validation', () => { }); test('should validate and throw error when method on config is invalid', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', method: 'https', }; @@ -110,7 +108,7 @@ describe('config validation', () => { }); test('config validation passes when a url is specified', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', }; expect(validateConfig(actionType, config)).toEqual({ @@ -120,6 +118,8 @@ describe('config validation', () => { }); test('config validation passes when valid headers are provided', () => { + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record = { url: 'http://mylisteningserver:9200/endpoint', headers: { @@ -133,7 +133,7 @@ describe('config validation', () => { }); test('should validate and throw error when headers on config is invalid', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', headers: 'application/json', }; @@ -147,6 +147,8 @@ describe('config validation', () => { }); test('config validation passes when kibana config whitelists the url', () => { + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record = { url: 'http://mylisteningserver.com:9200/endpoint', headers: { @@ -171,6 +173,8 @@ describe('config validation', () => { }, }); + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record = { url: 'http://mylisteningserver.com:9200/endpoint', headers: { @@ -188,12 +192,12 @@ describe('config validation', () => { describe('params validation', () => { test('param validation passes when no fields are provided as none are required', () => { - const params: Record = {}; + const params: Record = {}; expect(validateParams(actionType, params)).toEqual({}); }); test('params validation passes when a valid body is provided', () => { - const params: Record = { + const params: Record = { body: 'count: {{ctx.payload.hits.total}}', }; expect(validateParams(actionType, params)).toEqual({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 6173edc2df15a..0191ff1d9dbc1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -160,7 +160,7 @@ export async function executor( } // Action Executor Result w/ internationalisation -function successResult(actionId: string, data: any): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 51e87dbd75b48..161a6c31d4e59 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -7,7 +7,7 @@ import { configSchema } from './config'; describe('config validation', () => { test('action defaults', () => { - const config: Record = {}; + const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "enabled": true, @@ -23,7 +23,7 @@ describe('config validation', () => { }); test('action with preconfigured actions', () => { - const config: Record = { + const config: Record = { preconfigured: [ { id: 'my-slack1', diff --git a/x-pack/plugins/actions/server/constants/plugin.ts b/x-pack/plugins/actions/server/constants/plugin.ts index 68082ccaa1399..7d20eb6990247 100644 --- a/x-pack/plugins/actions/server/constants/plugin.ts +++ b/x-pack/plugins/actions/server/constants/plugin.ts @@ -9,6 +9,7 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/const export const PLUGIN = { ID: 'actions', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => i18n.translate('xpack.actions.appName', { defaultMessage: 'Actions', diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 4a9ddf412b7cc..ac324587c5f4a 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from '../../../../src/core/server'; +import { SavedObjectsClientContract, KibanaRequest } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { GetBasePathFunction, @@ -15,7 +15,7 @@ import { interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; - getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract; + getScopedSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract; getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; actionTypeRegistry: ActionTypeRegistryContract; @@ -24,7 +24,7 @@ interface CreateExecuteFunctionOptions { export interface ExecuteOptions { id: string; - params: Record; + params: Record; spaceId: string; apiKey: string | null; } @@ -52,7 +52,7 @@ export function createExecuteFunction({ // Since we're using API keys and accessing elasticsearch can only be done // via a request, we're faking one with the proper authorization headers. - const fakeRequest: any = { + const fakeRequest: unknown = { headers: requestHeaders, getBasePath: () => getBasePath(spaceId), path: '/', @@ -67,7 +67,7 @@ export function createExecuteFunction({ }, }; - const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); + const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest as KibanaRequest); const actionTypeId = await getActionTypeId(id); actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index a33fb8830a930..ac574decdba71 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -32,7 +32,7 @@ export interface ActionExecutorContext { export interface ExecuteOptions { actionId: string; request: KibanaRequest; - params: Record; + params: Record; } export type ActionExecutorContract = PublicMethodsOf; @@ -93,9 +93,9 @@ export class ActionExecutor { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); const actionType = actionTypeRegistry.get(actionTypeId); - let validatedParams: Record; - let validatedConfig: Record; - let validatedSecrets: Record; + let validatedParams: Record; + let validatedConfig: Record; + let validatedSecrets: Record; try { validatedParams = validateParams(actionType, params); @@ -174,8 +174,8 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string { interface ActionInfo { actionTypeId: string; name: string; - config: any; - secrets: any; + config: unknown; + secrets: unknown; } async function getActionInfo( diff --git a/x-pack/plugins/actions/server/lib/executor_error.ts b/x-pack/plugins/actions/server/lib/executor_error.ts index 56ccbf14e6a45..c25033b2a27a2 100644 --- a/x-pack/plugins/actions/server/lib/executor_error.ts +++ b/x-pack/plugins/actions/server/lib/executor_error.ts @@ -5,9 +5,9 @@ */ export class ExecutorError extends Error { - readonly data?: any; + readonly data?: unknown; readonly retry: boolean | Date; - constructor(message?: string, data?: any, retry: boolean | Date = false) { + constructor(message?: string, data?: unknown, retry: boolean | Date = false) { super(message); this.data = data; this.retry = retry; diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index eb10e69a444e8..0a474ec3ae3ea 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -5,16 +5,16 @@ */ import { ActionType } from '../types'; -import { BehaviorSubject } from 'rxjs'; +import { Subject } from 'rxjs'; import { LicenseState, ILicenseState } from './license_state'; import { licensingMock } from '../../../licensing/server/mocks'; import { ILicense } from '../../../licensing/server'; describe('checkLicense()', () => { - let getRawLicense: any; + const getRawLicense = jest.fn(); beforeEach(() => { - getRawLicense = jest.fn(); + jest.resetAllMocks(); }); describe('status is LICENSE_STATUS_INVALID', () => { @@ -53,7 +53,7 @@ describe('checkLicense()', () => { }); describe('isLicenseValidForActionType', () => { - let license: BehaviorSubject; + let license: Subject; let licenseState: ILicenseState; const fooActionType: ActionType = { id: 'foo', @@ -63,7 +63,7 @@ describe('isLicenseValidForActionType', () => { }; beforeEach(() => { - license = new BehaviorSubject(null as any); + license = new Subject(); licenseState = new LicenseState(license); }); @@ -75,7 +75,7 @@ describe('isLicenseValidForActionType', () => { }); test('should return false when license not available', () => { - license.next({ isAvailable: false } as any); + license.next(createUnavailableLicense()); expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ isValid: false, reason: 'unavailable', @@ -114,7 +114,7 @@ describe('isLicenseValidForActionType', () => { }); describe('ensureLicenseForActionType()', () => { - let license: BehaviorSubject; + let license: Subject; let licenseState: ILicenseState; const fooActionType: ActionType = { id: 'foo', @@ -124,7 +124,7 @@ describe('ensureLicenseForActionType()', () => { }; beforeEach(() => { - license = new BehaviorSubject(null as any); + license = new Subject(); licenseState = new LicenseState(license); }); @@ -137,7 +137,7 @@ describe('ensureLicenseForActionType()', () => { }); test('should throw when license not available', () => { - license.next({ isAvailable: false } as any); + license.next(createUnavailableLicense()); expect(() => licenseState.ensureLicenseForActionType(fooActionType) ).toThrowErrorMatchingInlineSnapshot( @@ -175,3 +175,9 @@ describe('ensureLicenseForActionType()', () => { licenseState.ensureLicenseForActionType(fooActionType); }); }); + +function createUnavailableLicense() { + const unavailableLicense = licensingMock.createLicenseMock(); + unavailableLicense.isAvailable = false; + return unavailableLicense; +} diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index e2a6128aea203..08c5b90edbcb7 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -6,7 +6,7 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; -import { Logger, CoreStart } from '../../../../../src/core/server'; +import { Logger, CoreStart, KibanaRequest } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTypeDisabledError } from './errors'; @@ -60,7 +60,7 @@ export class TaskRunnerFactory { return { async run() { - const { spaceId, actionTaskParamsId } = taskInstance.params; + const { spaceId, actionTaskParamsId } = taskInstance.params as Record; const namespace = spaceIdToNamespace(spaceId); const { @@ -78,7 +78,7 @@ export class TaskRunnerFactory { // Since we're using API keys and accessing elasticsearch can only be done // via a request, we're faking one with the proper authorization headers. - const fakeRequest: any = { + const fakeRequest = ({ headers: requestHeaders, getBasePath: () => getBasePath(spaceId), path: '/', @@ -91,7 +91,7 @@ export class TaskRunnerFactory { url: '/', }, }, - }; + } as unknown) as KibanaRequest; let executorResult: ActionTypeExecutorResult; try { diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index b7d408985ed9f..1ccd25664374d 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -49,7 +49,7 @@ test('should validate when there are no individual validators', () => { }); test('should validate when validators return incoming value', () => { - const selfValidator = { validate: (value: any) => value }; + const selfValidator = { validate: (value: Record) => value }; const actionType: ActionType = { id: 'foo', name: 'bar', @@ -76,8 +76,8 @@ test('should validate when validators return incoming value', () => { }); test('should validate when validators return different values', () => { - const returnedValue: any = { something: { shaped: 'differently' } }; - const selfValidator = { validate: (value: any) => returnedValue }; + const returnedValue = { something: { shaped: 'differently' } }; + const selfValidator = { validate: () => returnedValue }; const actionType: ActionType = { id: 'foo', name: 'bar', @@ -105,7 +105,7 @@ test('should validate when validators return different values', () => { test('should throw with expected error when validators fail', () => { const erroringValidator = { - validate: (value: any) => { + validate: () => { throw new Error('test error'); }, }; diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.ts index 2306a726229e3..021c460f4c815 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.ts @@ -7,15 +7,15 @@ import Boom from 'boom'; import { ActionType } from '../types'; -export function validateParams(actionType: ActionType, value: any) { +export function validateParams(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'params', value); } -export function validateConfig(actionType: ActionType, value: any) { +export function validateConfig(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'config', value); } -export function validateSecrets(actionType: ActionType, value: any) { +export function validateSecrets(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'secrets', value); } @@ -24,33 +24,40 @@ type ValidKeys = 'params' | 'config' | 'secrets'; function validateWithSchema( actionType: ActionType, key: ValidKeys, - value: any -): Record { - if (actionType.validate == null) return value; - - let name; - try { - switch (key) { - case 'params': - name = 'action params'; - if (actionType.validate.params == null) return value; - return actionType.validate.params.validate(value); - - case 'config': - name = 'action type config'; - if (actionType.validate.config == null) return value; - return actionType.validate.config.validate(value); - - case 'secrets': - name = 'action type secrets'; - if (actionType.validate.secrets == null) return value; - return actionType.validate.secrets.validate(value); + value: unknown +): Record { + if (actionType.validate) { + let name; + try { + switch (key) { + case 'params': + name = 'action params'; + if (actionType.validate.params) { + return actionType.validate.params.validate(value); + } + break; + case 'config': + name = 'action type config'; + if (actionType.validate.config) { + return actionType.validate.config.validate(value); + } + + break; + case 'secrets': + name = 'action type secrets'; + if (actionType.validate.secrets) { + return actionType.validate.secrets.validate(value); + } + break; + default: + // should never happen, but left here for future-proofing + throw new Error(`invalid actionType validate key: ${key}`); + } + } catch (err) { + // we can't really i18n this yet, since the err.message isn't i18n'd itself + throw Boom.badRequest(`error validating ${name}: ${err.message}`); } - } catch (err) { - // we can't really i18n this yet, since the err.message isn't i18n'd itself - throw Boom.badRequest(`error validating ${name}: ${err.message}`); } - // should never happen, but left here for future-proofing - throw new Error(`invalid actionType validate key: ${key}`); + return value as Record; } diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index fa5b2f9399a4d..2b334953063d1 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; @@ -72,7 +72,9 @@ describe('Actions Plugin', () => { }); it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); expect(context.logger.get().warn).toHaveBeenCalledWith( 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' @@ -81,7 +83,9 @@ describe('Actions Plugin', () => { describe('routeHandlerContext.getActionsClient()', () => { it('should not throw error when ESO plugin not using a generated key', async () => { - await plugin.setup(coreSetup, { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, @@ -93,8 +97,8 @@ describe('Actions Plugin', () => { const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; expect(handler[0]).toEqual('actions'); - const actionsContextHandler = (await handler[1]( - { + const actionsContextHandler = ((await handler[1]( + ({ core: { savedObjects: { client: {}, @@ -103,32 +107,34 @@ describe('Actions Plugin', () => { adminClient: jest.fn(), }, }, - } as any, + } as unknown) as RequestHandlerContext, httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() - )) as any; - actionsContextHandler.getActionsClient(); + )) as unknown) as RequestHandlerContext['actions']; + actionsContextHandler!.getActionsClient(); }); it('should throw error when ESO plugin using a generated key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1); const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; expect(handler[0]).toEqual('actions'); - const actionsContextHandler = (await handler[1]( - { + const actionsContextHandler = ((await handler[1]( + ({ core: { savedObjects: { client: {}, }, }, - } as any, + } as unknown) as RequestHandlerContext, httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() - )) as any; - expect(() => actionsContextHandler.getActionsClient()).toThrowErrorMatchingInlineSnapshot( + )) as unknown) as RequestHandlerContext['actions']; + expect(() => actionsContextHandler!.getActionsClient()).toThrowErrorMatchingInlineSnapshot( `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); }); @@ -144,13 +150,17 @@ describe('Actions Plugin', () => { }; beforeEach(async () => { - setup = await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setup = await plugin.setup(coreSetup as any, pluginsSetup); }); it('should throw error when license type is invalid', async () => { expect(() => setup.registerType({ ...sampleActionType, + // we're faking an invalid value, this requires stripping the typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any minimumLicenseRequired: 'foo' as any, }) ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); @@ -211,7 +221,9 @@ describe('Actions Plugin', () => { describe('getActionsClientWithRequest()', () => { it('should not throw error when ESO plugin not using a generated key', async () => { - await plugin.setup(coreSetup, { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, @@ -224,7 +236,9 @@ describe('Actions Plugin', () => { }); it('should throw error when ESO plugin using generated key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); const pluginStart = plugin.start(coreStart, pluginsStart); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a8ab3bbb2fad2..280c14ca8c058 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -117,7 +117,10 @@ export class ActionsPlugin implements Plugin, Plugi this.preconfiguredActions = []; } - public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise { + public async setup( + core: CoreSetup, + plugins: ActionsPluginsSetup + ): Promise { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -182,7 +185,7 @@ export class ActionsPlugin implements Plugin, Plugi const usageCollection = plugins.usageCollection; if (usageCollection) { - core.getStartServices().then(async ([, startPlugins]: [CoreStart, any, any]) => { + core.getStartServices().then(async ([, startPlugins]) => { registerActionsUsageCollector(usageCollection, startPlugins.taskManager); initializeActionsTelemetry( @@ -299,7 +302,7 @@ export class ActionsPlugin implements Plugin, Plugi private createRouteHandlerContext = ( defaultKibanaIndex: string - ): IContextProvider, 'actions'> => { + ): IContextProvider, 'actions'> => { const { actionTypeRegistry, isESOUsingEphemeralEncryptionKey, preconfiguredActions } = this; return async function actionsRouteHandlerContext(context, request) { diff --git a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts index ec525adb8eab6..06f65ff23106c 100644 --- a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts @@ -7,12 +7,17 @@ import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { identity } from 'lodash'; import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { ActionType } from '../../common'; +import { ActionsClientMock, actionsClientMock } from '../actions_client.mock'; export function mockHandlerArguments( - { actionsClient, listTypes: listTypesRes = [] }: any, - req: any, + { + actionsClient = actionsClientMock.create(), + listTypes: listTypesRes = [], + }: { actionsClient?: ActionsClientMock; listTypes?: ActionType[] }, + req: unknown, res?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { const listTypes = jest.fn(() => listTypesRes); return [ ({ @@ -31,7 +36,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest, + req as KibanaRequest, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 22cf0dd7f8ace..1fa85c86e0651 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { createActionRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('createActionRoute', () => { it('creates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); @@ -40,10 +41,11 @@ describe('createActionRoute', () => { name: 'My name', actionTypeId: 'abc', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce(createResult), - }; + + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce(createResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -83,22 +85,22 @@ describe('createActionRoute', () => { it('ensures the license allows creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce({ - id: '1', - name: 'My name', - actionTypeId: 'abc', - config: { foo: true }, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); - const [context, req, res] = mockHandlerArguments(actionsClient, {}); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); await handler(context, req, res); @@ -107,7 +109,7 @@ describe('createActionRoute', () => { it('ensures the license check prevents creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -117,16 +119,16 @@ describe('createActionRoute', () => { const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce({ - id: '1', - name: 'My name', - actionTypeId: 'abc', - config: { foo: true }, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); - const [context, req, res] = mockHandlerArguments(actionsClient, {}); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); @@ -135,15 +137,14 @@ describe('createActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok', 'forbidden']); diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 0cd3143dc48d1..7f239bda41a60 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -36,9 +36,9 @@ export const createActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any>, + req: KibanaRequest>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts index 6fb526628cb02..e63989e27a57c 100644 --- a/x-pack/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/delete.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteActionRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../mocks'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('deleteActionRoute', () => { it('deletes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); @@ -35,9 +36,8 @@ describe('deleteActionRoute', () => { } `); - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -65,19 +65,21 @@ describe('deleteActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); const [, handler] = router.delete.mock.calls[0]; - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(actionsClient, { - params: { id: '1' }, - }); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + } + ); await handler(context, req, res); @@ -86,7 +88,7 @@ describe('deleteActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -96,13 +98,15 @@ describe('deleteActionRoute', () => { const [, handler] = router.delete.mock.calls[0]; - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(actionsClient, { - id: '1', - }); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + id: '1', + } + ); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index ffd1f0faabbab..b76aa27fa5d9b 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -37,9 +37,9 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 3a3ed1257f576..1cd6343a39dcf 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -5,7 +5,7 @@ */ import { executeActionRoute } from './execute'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { ActionExecutorContract, verifyApiAccess, ActionTypeDisabledError } from '../lib'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('executeActionRoute', () => { it('executes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -77,7 +77,7 @@ describe('executeActionRoute', () => { it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -115,7 +115,7 @@ describe('executeActionRoute', () => { it('ensures the license allows action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -147,7 +147,7 @@ describe('executeActionRoute', () => { it('ensures the license check prevents action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -183,7 +183,7 @@ describe('executeActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 52d0aa706e703..79d3c1dfb8d22 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -43,9 +43,9 @@ export const executeActionRoute = ( }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, TypeOf, any>, + req: KibanaRequest, unknown, TypeOf>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); const { params } = req.body; const { id } = req.params; diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index f4e834a5b767c..f701be579d99d 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -5,10 +5,11 @@ */ import { getActionRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('getActionRoute', () => { it('gets an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); @@ -41,10 +42,11 @@ describe('getActionRoute', () => { actionTypeId: '2', name: 'action name', config: {}, + isPreconfigured: false, }; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce(getResult), - }; + + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce(getResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -60,6 +62,7 @@ describe('getActionRoute', () => { "actionTypeId": "2", "config": Object {}, "id": "1", + "isPreconfigured": false, "name": "action name", }, } @@ -75,23 +78,23 @@ describe('getActionRoute', () => { it('ensures the license allows getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); const [, handler] = router.get.mock.calls[0]; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce({ - id: '1', - actionTypeId: '2', - name: 'action name', - config: {}, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); const [context, req, res] = mockHandlerArguments( - actionsClient, + { actionsClient }, { params: { id: '1' }, }, @@ -105,7 +108,7 @@ describe('getActionRoute', () => { it('ensures the license check prevents getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -115,17 +118,17 @@ describe('getActionRoute', () => { const [, handler] = router.get.mock.calls[0]; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce({ - id: '1', - actionTypeId: '2', - name: 'action name', - config: {}, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); const [context, req, res] = mockHandlerArguments( - actionsClient, + { actionsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index cd29e54556b02..30e4289ed30c8 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -32,9 +32,9 @@ export const getActionRoute = (router: IRouter, licenseState: ILicenseState) => }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts index 6499427b8c1a5..e00054fd3746f 100644 --- a/x-pack/plugins/actions/server/routes/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -5,10 +5,11 @@ */ import { getAllActionRoute } from './get_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('getAllActionRoute', () => { it('get all actions with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -36,9 +37,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); @@ -57,7 +57,7 @@ describe('getAllActionRoute', () => { it('ensures the license allows getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -72,9 +72,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); @@ -85,7 +84,7 @@ describe('getAllActionRoute', () => { it('ensures the license check prevents getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -104,9 +103,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts index c70a13bc01c9f..f6745427f74c7 100644 --- a/x-pack/plugins/actions/server/routes/get_all.ts +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -25,9 +25,9 @@ export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts index 76fb636a75be7..205752d5a49d1 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts @@ -5,10 +5,11 @@ */ import { listActionTypesRoute } from './list_action_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { LicenseType } from '../../../../plugins/licensing/server'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('listActionTypesRoute', () => { it('lists action types with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -41,6 +42,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; @@ -51,7 +55,10 @@ describe('listActionTypesRoute', () => { "body": Array [ Object { "enabled": true, + "enabledInConfig": true, + "enabledInLicense": true, "id": "1", + "minimumLicenseRequired": "gold", "name": "name", }, ], @@ -67,7 +74,7 @@ describe('listActionTypesRoute', () => { it('ensures the license allows listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -87,6 +94,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; @@ -105,7 +115,7 @@ describe('listActionTypesRoute', () => { it('ensures the license check prevents listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -129,6 +139,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts index 71dcbd2e19bf7..50bc7405f4d6b 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -25,9 +25,9 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index 161fb4398af1d..6459a34bf0737 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { updateActionRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('updateActionRoute', () => { it('updates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -40,11 +41,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -86,7 +87,7 @@ describe('updateActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -97,11 +98,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -125,7 +126,7 @@ describe('updateActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -140,11 +141,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -168,15 +169,14 @@ describe('updateActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); const [, handler] = router.put.mock.calls[0]; - const actionsClient = { - update: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: {}, body: {} }, [ 'ok', diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index 263df678f293d..45ced77be922e 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -39,9 +39,9 @@ export const updateActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, TypeOf, any>, + req: KibanaRequest, unknown, TypeOf>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 92e38d77314f8..9dcfdb81f5ebb 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectAttributes } from '../../../../src/core/server'; +import { + SavedObjectsClientContract, + SavedObjectAttributes, + KibanaRequest, +} from '../../../../src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { ActionsClient } from './actions_client'; import { LicenseType } from '../../licensing/common/types'; export type WithoutQueryAndParams = Pick>; -export type GetServicesFunction = (request: any) => Services; +export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export interface Services { - callCluster(path: string, opts: any): Promise; + callCluster(path: string, opts: unknown): Promise; savedObjectsClient: SavedObjectsClientContract; } @@ -45,8 +49,12 @@ export interface ActionsConfigType { export interface ActionTypeExecutorOptions { actionId: string; services: Services; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; } @@ -54,11 +62,15 @@ export interface ActionResult { id: string; actionTypeId: string; name: string; - config: Record; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config?: Record; isPreconfigured: boolean; } export interface PreConfiguredAction extends ActionResult { + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record; } @@ -72,6 +84,8 @@ export interface ActionTypeExecutorResult { status: 'ok' | 'error'; message?: string; serviceMessage?: string; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any data?: any; retry?: null | boolean | Date; } @@ -82,7 +96,7 @@ export type ExecutorType = ( ) => Promise; interface ValidatorType { - validate(value: any): any; + validate(value: unknown): Record; } export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; @@ -108,6 +122,13 @@ export interface RawAction extends SavedObjectAttributes { export interface ActionTaskParams extends SavedObjectAttributes { actionId: string; + // Saved Objects won't allow us to enforce unknown rather than any + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; apiKey?: string; } + +export interface ActionTaskExecutorParams { + spaceId: string; + actionTaskParamsId: string; +} diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index eabb38e61d17d..6996ba629f8da 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -55,6 +55,8 @@ export async function getTotalCount(callCluster: APICaller, kibanaIndex: string) 0 ), countByType: Object.keys(searchResult.aggregations.byActionTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: searchResult.aggregations.byActionTypeId.value.types[key], diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx index 41ef863c00e44..ef4a0f76de9ed 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -12,7 +12,7 @@ import { EuiIcon, EuiSpacer, EuiText, - EuiKeyPadMenuItemButton, + EuiKeyPadMenuItem, } from '@elastic/eui'; import { txtChangeButton } from './i18n'; import './action_wizard.scss'; @@ -180,7 +180,7 @@ const ActionFactorySelector: React.FC = ({ return ( {actionFactories.map(actionFactory => ( - = ({ onClick={() => onActionFactorySelected(actionFactory)} > {actionFactory.iconType && } - + ))} ); diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 8f28c8fbaed7f..b410b6aa0187e 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -28,6 +28,8 @@ export interface Alert { consumer: string; schedule: IntervalSchedule; actions: AlertAction[]; + // This will have to remain `any` until we can extend Alert Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; scheduledTaskId?: string; createdBy: string | null; diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerting/server/alert_type_registry.test.ts index b51286281571e..f9df390242cd4 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.test.ts @@ -231,7 +231,7 @@ function alertTypeWithVariables(id: string, context: string, state: string): Ale name: `${id}-name`, actionGroups: [], defaultActionGroupId: id, - executor: (params: any): any => {}, + async executor() {}, }; if (!context && !state) { diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerting/server/alert_type_registry.ts index a2be43f9dacbd..55e39b6a817db 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.ts @@ -77,7 +77,7 @@ export class AlertTypeRegistry { } } -function normalizedActionVariables(actionVariables: any) { +function normalizedActionVariables(actionVariables: AlertType['actionVariables']) { return { context: actionVariables?.context ?? [], state: actionVariables?.state ?? [], diff --git a/x-pack/plugins/alerting/server/alerts_client.mock.ts b/x-pack/plugins/alerting/server/alerts_client.mock.ts index 3189fa214d5f7..1848b3432ae5a 100644 --- a/x-pack/plugins/alerting/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerting/server/alerts_client.mock.ts @@ -7,9 +7,10 @@ import { AlertsClient } from './alerts_client'; type Schema = PublicMethodsOf; +export type AlertsClientMock = jest.Mocked; const createAlertsClientMock = () => { - const mocked: jest.Mocked = { + const mocked: AlertsClientMock = { create: jest.fn(), get: jest.fn(), getAlertState: jest.fn(), @@ -27,6 +28,8 @@ const createAlertsClientMock = () => { return mocked; }; -export const alertsClientMock = { +export const alertsClientMock: { + create: () => AlertsClientMock; +} = { create: createAlertsClientMock, }; diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index a9ff5ee8ecdc6..6c7b93aa64003 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -5,7 +5,7 @@ */ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { AlertsClient } from './alerts_client'; +import { AlertsClient, CreateOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; @@ -45,6 +45,7 @@ beforeEach(() => { }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date { constructor() { return mockedDate; @@ -54,7 +55,7 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); } }; -function getMockData(overwrites: Record = {}) { +function getMockData(overwrites: Record = {}): CreateOptions['data'] { return { enabled: true, name: 'abc', diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 3e4c26d3444c9..ff501055ba9fe 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -24,6 +24,7 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, + RawAlertAction, } from './types'; import { validateAlertTypeParams } from './lib'; import { @@ -83,7 +84,7 @@ export interface FindResult { data: SanitizedAlert[]; } -interface CreateOptions { +export interface CreateOptions { data: Omit< Alert, | 'id' @@ -109,7 +110,7 @@ interface UpdateOptions { tags: string[]; schedule: IntervalSchedule; actions: NormalizedAlertAction[]; - params: Record; + params: Record; throttle: string | null; }; } @@ -172,7 +173,7 @@ export class AlertsClient { createdBy: username, updatedBy: username, createdAt: new Date().toISOString(), - params: validatedAlertTypeParams, + params: validatedAlertTypeParams as RawAlert['params'], muteAll: false, mutedInstanceIds: [], }; @@ -337,7 +338,7 @@ export class AlertsClient { ...attributes, ...data, ...apiKeyAttributes, - params: validatedAlertTypeParams, + params: validatedAlertTypeParams as RawAlert['params'], actions, updatedBy: username, }, @@ -667,7 +668,7 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const actionMap = new Map(); + const actionMap = new Map(); // map preconfigured actions for (const alertAction of alertActions) { const action = this.preconfiguredActions.find( @@ -712,8 +713,8 @@ export class AlertsClient { // if action is a save object, than actionTypeId should be under attributes property // if action is a preconfigured, than actionTypeId is the action property const actionTypeId = actionIds.find(actionId => actionId === id) - ? actionMapValue.attributes.actionTypeId - : actionMapValue.actionTypeId; + ? (actionMapValue as SavedObject>).attributes.actionTypeId + : (actionMapValue as RawAlertAction).actionTypeId; return { ...alertAction, actionRef, diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index 951d18a33b35f..e5aa0a674eccf 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -11,16 +11,13 @@ import { taskManagerMock } from '../../../plugins/task_manager/server/task_manag import { KibanaRequest } from '../../../../src/core/server'; import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { AuthenticatedUser } from '../../../plugins/security/public'; +import { securityMock } from '../../../plugins/security/server/mocks'; jest.mock('./alerts_client'); const savedObjectsClient = savedObjectsClientMock.create(); -const securityPluginSetup = { - authc: { - grantAPIKeyAsInternalUser: jest.fn(), - getCurrentUser: jest.fn(), - }, -}; +const securityPluginSetup = securityMock.createSetup(); const alertsClientFactoryParams: jest.Mocked = { logger: loggingServiceMock.create().get(), taskManager: taskManagerMock.start(), @@ -30,7 +27,7 @@ const alertsClientFactoryParams: jest.Mocked = { encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.createStart(), preconfiguredActions: [], }; -const fakeRequest: Request = { +const fakeRequest = ({ headers: {}, getBasePath: () => '', path: '/', @@ -44,7 +41,7 @@ const fakeRequest: Request = { }, }, getSavedObjectsClient: () => savedObjectsClient, -} as any; +} as unknown) as Request; beforeEach(() => { jest.resetAllMocks(); @@ -86,12 +83,14 @@ test('getUserName() returns a name when security is enabled', async () => { const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce({ username: 'bob' }); + securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({ + username: 'bob', + } as unknown) as AuthenticatedUser); const userNameResult = await constructorCall.getUserName(); expect(userNameResult).toEqual('bob'); }); @@ -121,7 +120,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => { const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; @@ -129,11 +128,12 @@ test('createAPIKey() returns an API key when security is enabled', async () => { securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({ api_key: '123', id: 'abc', + name: '', }); const createAPIKeyResult = await constructorCall.createAPIKey(); expect(createAPIKeyResult).toEqual({ apiKeysEnabled: true, - result: { api_key: '123', id: 'abc' }, + result: { api_key: '123', id: 'abc', name: '' }, }); }); @@ -141,7 +141,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error', const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; diff --git a/x-pack/plugins/alerting/server/constants/plugin.ts b/x-pack/plugins/alerting/server/constants/plugin.ts index 173aa50013b40..9c276ed1d75de 100644 --- a/x-pack/plugins/alerting/server/constants/plugin.ts +++ b/x-pack/plugins/alerting/server/constants/plugin.ts @@ -9,6 +9,8 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/const export const PLUGIN = { ID: 'alerting', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + // all plugins seem to use getI18nName with any + // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => i18n.translate('xpack.alerting.appName', { defaultMessage: 'Alerting', diff --git a/x-pack/plugins/alerting/server/lib/license_state.test.ts b/x-pack/plugins/alerting/server/lib/license_state.test.ts index 9bbd619dc4868..cbab98a6311dd 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerting/server/lib/license_state.test.ts @@ -9,10 +9,10 @@ import { LicenseState } from './license_state'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; describe('license_state', () => { - let getRawLicense: any; + const getRawLicense = jest.fn(); beforeEach(() => { - getRawLicense = jest.fn(); + jest.resetAllMocks(); }); describe('status is LICENSE_STATUS_INVALID', () => { diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts index 248d896c06ac2..b6dcb522f0925 100644 --- a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts +++ b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts @@ -5,15 +5,15 @@ */ import Boom from 'boom'; -import { AlertType } from '../types'; +import { AlertType, AlertExecutorOptions } from '../types'; -export function validateAlertTypeParams>( +export function validateAlertTypeParams( alertType: AlertType, - params: T -): T { + params: Record +): AlertExecutorOptions['params'] { const validator = alertType.validate && alertType.validate.params; if (!validator) { - return params; + return params as AlertExecutorOptions['params']; } try { diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 74a1f2349180e..267e68930a5d7 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertingPlugin } from './plugin'; +import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../../plugins/licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; +import { KibanaRequest, CoreSetup } from 'kibana/server'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -20,19 +21,19 @@ describe('Alerting Plugin', () => { const coreSetup = coreMock.createSetup(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); @@ -58,34 +59,34 @@ describe('Alerting Plugin', () => { const coreSetup = coreMock.createSetup(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); const startContract = plugin.start( - coreMock.createStart() as any, - { + coreMock.createStart() as ReturnType, + ({ actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), }, - } as any + } as unknown) as AlertingPluginsStart ); expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); expect(() => - startContract.getAlertsClientWithRequest({} as any) + startContract.getAlertsClientWithRequest({} as KibanaRequest) ).toThrowErrorMatchingInlineSnapshot( `"Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); @@ -101,33 +102,33 @@ describe('Alerting Plugin', () => { usingEphemeralEncryptionKey: false, }; await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); const startContract = plugin.start( - coreMock.createStart() as any, - { + coreMock.createStart() as ReturnType, + ({ actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), }, spaces: () => null, - } as any + } as unknown) as AlertingPluginsStart ); - const fakeRequest = { + const fakeRequest = ({ headers: {}, getBasePath: () => '', path: '/', @@ -141,8 +142,8 @@ describe('Alerting Plugin', () => { }, }, getSavedObjectsClient: jest.fn(), - }; - await startContract.getAlertsClientWithRequest(fakeRequest as any); + } as unknown) as KibanaRequest; + await startContract.getAlertsClientWithRequest(fakeRequest); }); }); }); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index ad39d09bd6d3d..35ebafce9dc67 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -117,7 +117,10 @@ export class AlertingPlugin { .toPromise(); } - public async setup(core: CoreSetup, plugins: AlertingPluginsSetup): Promise { + public async setup( + core: CoreSetup, + plugins: AlertingPluginsSetup + ): Promise { this.licenseState = new LicenseState(plugins.licensing.license$); this.spaces = plugins.spaces?.spacesService; this.security = plugins.security; @@ -157,7 +160,7 @@ export class AlertingPlugin { const usageCollection = plugins.usageCollection; if (usageCollection) { - core.getStartServices().then(async ([, startPlugins]: [CoreStart, any, any]) => { + core.getStartServices().then(async ([, startPlugins]) => { registerAlertsUsageCollector(usageCollection, startPlugins.taskManager); initializeAlertingTelemetry( @@ -246,7 +249,7 @@ export class AlertingPlugin { } private createRouteHandlerContext = (): IContextProvider< - RequestHandler, + RequestHandler, 'alerting' > => { const { alertTypeRegistry, alertsClientFactory } = this; diff --git a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts index 5a1d680eb06f3..6fb3df8446f5c 100644 --- a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts @@ -4,16 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IClusterClient, +} from 'kibana/server'; import { identity } from 'lodash'; import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../alerts_client.mock'; +import { alertsClientMock, AlertsClientMock } from '../alerts_client.mock'; +import { AlertType } from '../../common'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; export function mockHandlerArguments( - { alertsClient, listTypes: listTypesRes = [], elasticsearch }: any, - req: any, + { + alertsClient = alertsClientMock.create(), + listTypes: listTypesRes = [], + elasticsearch = elasticsearchServiceMock.createSetup(), + }: { + alertsClient?: AlertsClientMock; + listTypes?: AlertType[]; + elasticsearch?: jest.Mocked<{ + adminClient: jest.Mocked; + }>; + }, + req: unknown, res?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { const listTypes = jest.fn(() => listTypesRes); return [ ({ @@ -25,7 +42,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest, + req as KibanaRequest, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerting/server/routes/create.test.ts index c6a0da2bd9191..a4910495c8a40 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerting/server/routes/create.test.ts @@ -5,7 +5,7 @@ */ import { createAlertRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -68,7 +68,7 @@ describe('createAlertRoute', () => { it('creates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -134,7 +134,7 @@ describe('createAlertRoute', () => { it('ensures the license allows creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -142,7 +142,7 @@ describe('createAlertRoute', () => { alertsClient.create.mockResolvedValueOnce(createResult); - const [context, req, res] = mockHandlerArguments(alertsClient, {}); + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}); await handler(context, req, res); @@ -151,7 +151,7 @@ describe('createAlertRoute', () => { it('ensures the license check prevents creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -163,7 +163,7 @@ describe('createAlertRoute', () => { alertsClient.create.mockResolvedValueOnce(createResult); - const [context, req, res] = mockHandlerArguments(alertsClient, {}); + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/alerting/server/routes/create.ts b/x-pack/plugins/alerting/server/routes/create.ts index f08460ffcb453..0c038b6490483 100644 --- a/x-pack/plugins/alerting/server/routes/create.ts +++ b/x-pack/plugins/alerting/server/routes/create.ts @@ -54,9 +54,9 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any>, + req: KibanaRequest>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerting/server/routes/delete.test.ts index 36bb485817c15..416628d015b5a 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerting/server/routes/delete.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteAlertRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('deleteAlertRoute', () => { it('deletes an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -66,7 +66,7 @@ describe('deleteAlertRoute', () => { it('ensures the license allows deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -74,9 +74,12 @@ describe('deleteAlertRoute', () => { alertsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(alertsClient, { - params: { id: '1' }, - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + } + ); await handler(context, req, res); @@ -85,7 +88,7 @@ describe('deleteAlertRoute', () => { it('ensures the license check prevents deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -97,9 +100,12 @@ describe('deleteAlertRoute', () => { alertsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(alertsClient, { - id: '1', - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + id: '1', + } + ); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/alerting/server/routes/delete.ts b/x-pack/plugins/alerting/server/routes/delete.ts index 8d77c9b395e59..7f6600b1ec48e 100644 --- a/x-pack/plugins/alerting/server/routes/delete.ts +++ b/x-pack/plugins/alerting/server/routes/delete.ts @@ -33,9 +33,9 @@ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerting/server/routes/disable.test.ts index 622b562ec6911..fde095e9145b6 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerting/server/routes/disable.test.ts @@ -5,7 +5,7 @@ */ import { disableAlertRoute } from './disable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('disableAlertRoute', () => { it('disables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); disableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/disable.ts b/x-pack/plugins/alerting/server/routes/disable.ts index fcc7116a697b1..c7e7b1001f82d 100644 --- a/x-pack/plugins/alerting/server/routes/disable.ts +++ b/x-pack/plugins/alerting/server/routes/disable.ts @@ -33,9 +33,9 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) = }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerting/server/routes/enable.test.ts index 5a7b027e8ef54..e4e89e3f06380 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerting/server/routes/enable.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { enableAlertRoute } from './enable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('enableAlertRoute', () => { it('enables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); enableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/enable.ts b/x-pack/plugins/alerting/server/routes/enable.ts index 2283ae4a4c765..3ed4fb0739d3d 100644 --- a/x-pack/plugins/alerting/server/routes/enable.ts +++ b/x-pack/plugins/alerting/server/routes/enable.ts @@ -35,9 +35,9 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/find.test.ts index 391d6df3f9931..cc601bd42b8ca 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findAlertRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -24,7 +24,7 @@ beforeEach(() => { describe('findAlertRoute', () => { it('finds alerts with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -95,7 +95,7 @@ describe('findAlertRoute', () => { it('ensures the license allows finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -108,13 +108,16 @@ describe('findAlertRoute', () => { data: [], }); - const [context, req, res] = mockHandlerArguments(alertsClient, { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + per_page: 1, + page: 1, + default_search_operator: 'OR', + }, + } + ); await handler(context, req, res); @@ -123,7 +126,7 @@ describe('findAlertRoute', () => { it('ensures the license check prevents finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerting/server/routes/find.ts index 0787f5c6b5ad6..c723419a965c5 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerting/server/routes/find.ts @@ -55,9 +55,9 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any>, + req: KibanaRequest, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerting/server/routes/get.test.ts index 4506700c6d9cc..7335f13c85a4d 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerting/server/routes/get.test.ts @@ -5,7 +5,7 @@ */ import { getAlertRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -55,7 +55,7 @@ describe('getAlertRoute', () => { it('gets an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; @@ -90,7 +90,7 @@ describe('getAlertRoute', () => { it('ensures the license allows getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); @@ -99,7 +99,7 @@ describe('getAlertRoute', () => { alertsClient.get.mockResolvedValueOnce(mockedAlert); const [context, req, res] = mockHandlerArguments( - alertsClient, + { alertsClient }, { params: { id: '1' }, }, @@ -113,7 +113,7 @@ describe('getAlertRoute', () => { it('ensures the license check prevents getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -126,7 +126,7 @@ describe('getAlertRoute', () => { alertsClient.get.mockResolvedValueOnce(mockedAlert); const [context, req, res] = mockHandlerArguments( - alertsClient, + { alertsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/alerting/server/routes/get.ts b/x-pack/plugins/alerting/server/routes/get.ts index 39fbe64b62182..6d652d1304f65 100644 --- a/x-pack/plugins/alerting/server/routes/get.ts +++ b/x-pack/plugins/alerting/server/routes/get.ts @@ -33,9 +33,9 @@ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts index eb51c96b88e5e..20a420ca00986 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts @@ -5,10 +5,10 @@ */ import { getAlertStateRoute } from './get_alert_state'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { SavedObjectsErrorHelpers } from 'src/core/server/saved_objects'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; import { alertsClientMock } from '../alerts_client.mock'; const alertsClient = alertsClientMock.create(); @@ -41,7 +41,7 @@ describe('getAlertStateRoute', () => { it('gets alert state', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -84,7 +84,7 @@ describe('getAlertStateRoute', () => { it('returns NO-CONTENT when alert exists but has no task state yet', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -127,7 +127,7 @@ describe('getAlertStateRoute', () => { it('returns NOT-FOUND when alert is not found', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.ts index c5493c4abf57a..552bfea22a42b 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.ts @@ -33,9 +33,9 @@ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 42c83a7c04deb..7c50fbf561e59 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -5,7 +5,7 @@ */ import { healthRoute } from './health'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { verifyApiAccess } from '../lib/license_api_access'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('healthRoute', () => { it('registers the route', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -35,7 +35,7 @@ describe('healthRoute', () => { }); it('queries the usage api', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -64,7 +64,7 @@ describe('healthRoute', () => { }); it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -88,7 +88,7 @@ describe('healthRoute', () => { }); it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -112,7 +112,7 @@ describe('healthRoute', () => { }); it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -136,7 +136,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -162,7 +162,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -188,7 +188,7 @@ describe('healthRoute', () => { }); it('evaluates security and tls enabled to mean that the user can generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index fa2358a1f181c..bfdbc95a7d2da 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -39,9 +39,9 @@ export function healthRoute( }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); try { const { diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts index 723fd86fca8b5..37b52f1ec7923 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts @@ -5,7 +5,7 @@ */ import { listAlertTypesRoute } from './list_alert_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('listAlertTypesRoute', () => { it('lists alert types with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -47,6 +47,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + actionVariables: [], }, ]; @@ -62,6 +63,7 @@ describe('listAlertTypesRoute', () => { "name": "Default", }, ], + "actionVariables": Array [], "defaultActionGroupId": "default", "id": "1", "name": "name", @@ -79,7 +81,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license allows listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -99,6 +101,14 @@ describe('listAlertTypesRoute', () => { id: '1', name: 'name', enabled: true, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: [], }, ]; @@ -117,7 +127,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license check prevents listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -147,6 +157,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + actionVariables: [], }, ]; diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.ts index 455bc5e378b6d..7ab64cf932051 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.ts @@ -26,9 +26,9 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/mute_all.test.ts index 4c880e176d2df..5ef9e3694f8f4 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.test.ts @@ -5,7 +5,7 @@ */ import { muteAllAlertRoute } from './mute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAllAlertRoute', () => { it('mute an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.ts b/x-pack/plugins/alerting/server/routes/mute_all.ts index 29ef7d6b6b03b..d1b4322bd1ccb 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.ts @@ -33,9 +33,9 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) = }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts index 939864972c35d..2e6adedb76df9 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts @@ -5,7 +5,7 @@ */ import { muteAlertInstanceRoute } from './mute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAlertInstanceRoute', () => { it('mutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.ts b/x-pack/plugins/alerting/server/routes/mute_instance.ts index 7a071b1535dc7..fbdda62836d74 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.ts @@ -34,9 +34,9 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts index cd14e9b2e7172..1756dbd3fb41d 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { unmuteAllAlertRoute } from './unmute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('unmuteAllAlertRoute', () => { it('unmutes an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.ts b/x-pack/plugins/alerting/server/routes/unmute_all.ts index 81e28a81874cd..e09f2fe6b8b93 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.ts @@ -33,9 +33,9 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts index d74934f691710..9b9542c606741 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts @@ -5,7 +5,7 @@ */ import { unmuteAlertInstanceRoute } from './unmute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('unmuteAlertInstanceRoute', () => { it('unmutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.ts index de081ae7f1fcb..64ba22dc3ea0b 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.ts @@ -34,9 +34,9 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/update.test.ts index c3628617f861f..cd96f289b8714 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/update.test.ts @@ -5,7 +5,7 @@ */ import { updateAlertRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -45,7 +45,7 @@ describe('updateAlertRoute', () => { it('updates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -128,7 +128,7 @@ describe('updateAlertRoute', () => { it('ensures the license allows updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -171,7 +171,7 @@ describe('updateAlertRoute', () => { it('ensures the license check prevents updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerting/server/routes/update.ts index 45f7b26b521d4..7f07749311598 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerting/server/routes/update.ts @@ -56,9 +56,9 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, TypeOf, any>, + req: KibanaRequest, unknown, TypeOf>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts index 5e9821ac005e2..0347feb24a235 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts @@ -5,7 +5,7 @@ */ import { updateApiKeyRoute } from './update_api_key'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('updateApiKeyRoute', () => { it('updates api key for an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateApiKeyRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.ts b/x-pack/plugins/alerting/server/routes/update_api_key.ts index f70d30f0bb5da..9d0c34fc1a015 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.ts @@ -35,9 +35,9 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) = handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts index 9f5e3851aa29d..4be506b78493b 100644 --- a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts +++ b/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts @@ -7,10 +7,17 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; -import { SanitizedAlert, AlertTaskState, alertParamsSchema, alertStateSchema } from '../../common'; +import { + SanitizedAlert, + AlertTaskState, + alertParamsSchema, + alertStateSchema, + AlertTaskParams, +} from '../../common'; export interface AlertTaskInstance extends ConcreteTaskInstance { state: AlertTaskState; + params: AlertTaskParams; } const enumerateErrorFields = (e: t.Errors) => diff --git a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts b/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts index 1c4d8a42d2830..f5914fdf01a16 100644 --- a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts @@ -7,6 +7,7 @@ import { getNextRunAt } from './get_next_run_at'; const mockedNow = new Date('2019-06-03T18:55:25.982Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date extends global.Date { static now() { return mockedNow.getTime(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 1d4b12e96bc76..9c8cf4b1c968d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -5,7 +5,7 @@ */ import { pick, mapValues, omit, without } from 'lodash'; -import { Logger, SavedObject } from '../../../../../src/core/server'; +import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; @@ -93,7 +93,7 @@ export class TaskRunner { }, }; - return this.context.getServices(fakeRequest); + return this.context.getServices((fakeRequest as unknown) as KibanaRequest); } private getExecutionHandler( @@ -178,7 +178,7 @@ export class TaskRunner { }; eventLogger.startTiming(event); - let updatedAlertTypeState: void | Record; + let updatedAlertTypeState: void | Record; try { updatedAlertTypeState = await this.alertType.executor({ alertId, diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index e0cff58c4d40a..64f846d13c0bf 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -29,7 +29,7 @@ export function transformActionParams({ actionParams, state, }: TransformActionParamsOptions): AlertActionParams { - const result = cloneDeep(actionParams, (value: any) => { + const result = cloneDeep(actionParams, (value: unknown) => { if (!isString(value)) return; // when the list of variables we pass in here changes, diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerting/server/test_utils/index.ts index be9c5493ccf2b..e8089984a786a 100644 --- a/x-pack/plugins/alerting/server/test_utils/index.ts +++ b/x-pack/plugins/alerting/server/test_utils/index.ts @@ -5,7 +5,7 @@ */ interface Resolvable { - resolve: (arg?: T) => void; + resolve: (arg: T) => void; } /** @@ -13,12 +13,10 @@ interface Resolvable { * coordinating async tests. */ export function resolvable(): Promise & Resolvable { - let resolve: (arg?: T) => void; - const result = new Promise(r => { - resolve = r; - }) as any; - - result.resolve = (arg: T) => resolve(arg); - - return result; + let resolve: (arg: T) => void; + return Object.assign(new Promise(r => (resolve = r)), { + resolve(arg: T) { + return setTimeout(() => resolve(arg), 0); + }, + }); } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 739a0d0aece24..bc98cae65b4e6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,15 +7,22 @@ import { AlertInstance } from './alert_instance'; import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../src/core/server'; +import { + SavedObjectAttributes, + SavedObjectsClientContract, + KibanaRequest, +} from '../../../../src/core/server'; import { Alert, AlertActionParams, ActionGroup } from '../common'; import { AlertsClient } from './alerts_client'; export * from '../common'; +// This will have to remain `any` until we can extend Alert Executors with generics +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type State = Record; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type Context = Record; export type WithoutQueryAndParams = Pick>; -export type GetServicesFunction = (request: any) => Services; +export type GetServicesFunction = (request: KibanaRequest) => Services; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; @@ -29,6 +36,8 @@ declare module 'src/core/server' { } export interface Services { + // This will have to remain `any` until we can extend Alert Services with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any callCluster(path: string, opts: any): Promise; savedObjectsClient: SavedObjectsClientContract; } @@ -42,6 +51,8 @@ export interface AlertExecutorOptions { startedAt: Date; previousStartedAt: Date | null; services: AlertServices; + // This will have to remain `any` until we can extend Alert Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; state: State; spaceId: string; @@ -61,7 +72,7 @@ export interface AlertType { id: string; name: string; validate?: { - params?: { validate: (object: any) => any }; + params?: { validate: (object: unknown) => AlertExecutorOptions['params'] }; }; actionGroups: ActionGroup[]; defaultActionGroupId: ActionGroup['id']; diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 9c710fa3b3b8e..2edf1c1061864 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -5,6 +5,7 @@ */ import { APICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; const alertTypeMetric = { scripted_metric: { @@ -246,6 +247,8 @@ export async function getTotalCountAggregations(callCluster: APICaller, kibanaIn return { count_total: totalAlertsCount, count_by_type: Object.keys(results.aggregations.byAlertTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: results.aggregations.byAlertTypeId.value.types[key], @@ -284,7 +287,7 @@ export async function getTotalCountAggregations(callCluster: APICaller, kibanaIn } export async function getTotalCountInUse(callCluster: APICaller, kibanaInex: string) { - const searchResult = await callCluster('search', { + const searchResult: SearchResponse = await callCluster('search', { index: kibanaInex, rest_total_hits_as_int: true, body: { @@ -305,6 +308,8 @@ export async function getTotalCountInUse(callCluster: APICaller, kibanaInex: str 0 ), countByType: Object.keys(searchResult.aggregations.byAlertTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: searchResult.aggregations.byAlertTypeId.value.types[key], diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts index 33d1e1897e943..c209894fb6e89 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts @@ -6,6 +6,7 @@ import { ParamsSchema, Params } from './alert_type_params'; import { runTests } from './lib/core_query_types.test'; +import { TypeOf } from '@kbn/config-schema'; const DefaultParams: Writable> = { index: 'index-name', @@ -21,6 +22,7 @@ const DefaultParams: Writable> = { describe('alertType Params validate()', () => { runTests(ParamsSchema, DefaultParams); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; beforeEach(() => { params = { ...DefaultParams }; @@ -64,7 +66,7 @@ describe('alertType Params validate()', () => { return () => validate(); } - function validate(): any { + function validate(): TypeOf { return ParamsSchema.validate(params); } }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts index f83d7fa07cd2a..4a822156ebd06 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts @@ -30,12 +30,12 @@ export const ParamsSchema = schema.object( const betweenComparators = new Set(['between', 'notBetween']); // using direct type not allowed, circular reference, so body is typed to any -function validateParams(anyParams: any): string | undefined { +function validateParams(anyParams: unknown): string | undefined { // validate core query parts, return if it fails validation (returning string) const coreQueryValidated = validateCoreQueryBody(anyParams); if (coreQueryValidated) return coreQueryValidated; - const { thresholdComparator, threshold }: Params = anyParams; + const { thresholdComparator, threshold }: Params = anyParams as Params; if (betweenComparators.has(thresholdComparator) && threshold.length === 1) { return i18n.translate('xpack.alertingBuiltins.indexThreshold.invalidThreshold2ErrorMessage', { diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts index 109785b835bdf..6c9c3542aea03 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts @@ -19,7 +19,8 @@ const DefaultParams: Writable> = { timeWindowUnit: 'm', }; -export function runTests(schema: ObjectType, defaultTypeParams: Record): void { +export function runTests(schema: ObjectType, defaultTypeParams: Record): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; describe('coreQueryTypes', () => { @@ -186,7 +187,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record validate(); } - function validate(): any { + function validate(): unknown { return schema.validate(params); } } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts index 6e9c0072bf7b6..c8da61fb56d21 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts @@ -41,9 +41,15 @@ export type CoreQueryParams = TypeOf; // Meant to be used in a "subclass"'s schema body validator, so the // anyParams object is assumed to have been validated with the schema // above. -// Using direct type not allowed, circular reference, so body is typed to any. -export function validateCoreQueryBody(anyParams: any): string | undefined { - const { aggType, aggField, groupBy, termField, termSize }: CoreQueryParams = anyParams; +// Using direct type not allowed, circular reference, so body is typed to unknown. +export function validateCoreQueryBody(anyParams: unknown): string | undefined { + const { + aggType, + aggField, + groupBy, + termField, + termSize, + }: CoreQueryParams = anyParams as CoreQueryParams; if (aggType !== 'count' && !aggField) { return i18n.translate('xpack.alertingBuiltins.indexThreshold.aggTypeRequiredErrorMessage', { defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"', diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts index 1d9cc1c98bc01..9c4133be6f483 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; import { DEFAULT_GROUPS } from '../index'; import { getDateRangeInfo } from './date_range_info'; import { Logger, CallCluster } from '../../../types'; @@ -35,6 +36,8 @@ export async function timeSeriesQuery( const dateRangeInfo = getDateRangeInfo({ dateStart, dateEnd, window, interval }); // core query + // Constructing a typesafe ES query in JS is problematic, use any escapehatch for now + // eslint-disable-next-line @typescript-eslint/no-explicit-any const esQuery: any = { index, body: { @@ -122,7 +125,7 @@ export async function timeSeriesQuery( }; } - let esResult: any; + let esResult: SearchResponse; const logPrefix = 'indexThreshold timeSeriesQuery: callCluster'; logger.debug(`${logPrefix} call: ${JSON.stringify(esQuery)}`); @@ -147,7 +150,7 @@ export async function timeSeriesQuery( function getResultFromEs( isCountAgg: boolean, isGroupAgg: boolean, - esResult: Record + esResult: SearchResponse ): TimeSeriesResult { const aggregations = esResult?.aggregations || {}; diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts index fcbd49b26ffd0..ec164122032cb 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts @@ -6,6 +6,7 @@ import { TimeSeriesQuerySchema, TimeSeriesQuery } from './time_series_types'; import { runTests } from './core_query_types.test'; +import { TypeOf } from '@kbn/config-schema'; const DefaultParams: Writable> = { index: 'index-name', @@ -19,6 +20,7 @@ const DefaultParams: Writable> = { describe('TimeSeriesParams validate()', () => { runTests(TimeSeriesQuerySchema, DefaultParams); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; beforeEach(() => { params = { ...DefaultParams }; @@ -102,7 +104,7 @@ describe('TimeSeriesParams validate()', () => { return () => validate(); } - function validate(): any { + function validate(): TypeOf { return TimeSeriesQuerySchema.validate(params); } }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts index abe5d562027eb..40e6f187ce18f 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts @@ -48,13 +48,13 @@ export const TimeSeriesQuerySchema = schema.object( } ); -// using direct type not allowed, circular reference, so body is typed to any -function validateBody(anyParams: any): string | undefined { +// using direct type not allowed, circular reference, so body is typed to unknown +function validateBody(anyParams: unknown): string | undefined { // validate core query parts, return if it fails validation (returning string) const coreQueryValidated = validateCoreQueryBody(anyParams); if (coreQueryValidated) return coreQueryValidated; - const { dateStart, dateEnd, interval }: TimeSeriesQuery = anyParams; + const { dateStart, dateEnd, interval } = anyParams as TimeSeriesQuery; // dates already validated in validateDate(), if provided const epochStart = dateStart ? Date.parse(dateStart) : undefined; diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts index 32d6409d9c9fb..5cc41671f6167 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts @@ -38,7 +38,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory ): Promise { service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts index c08450448b44c..ebcf6b4f0e45a 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts @@ -18,6 +18,7 @@ import { KibanaResponseFactory, IScopedClusterClient, } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Service } from '../../../types'; const bodySchema = schema.object({ @@ -40,7 +41,7 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute: ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory ): Promise { const pattern = req.body.pattern; @@ -102,12 +103,14 @@ async function getIndicesFromPattern( }, }, }; - const response = await dataClient.callAsCurrentUser('search', params); - if (response.status === 404 || !response.aggregations) { + const response: SearchResponse = await dataClient.callAsCurrentUser('search', params); + // TODO: Investigate when the status field might appear here, type suggests it shouldn't ever happen + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((response as any).status === 404 || !response.aggregations) { return []; } - return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + return (response.aggregations as IndiciesAggregation).indices.buckets.map(bucket => bucket.key); } async function getAliasesFromPattern( @@ -137,3 +140,9 @@ async function getAliasesFromPattern( return result; } + +interface IndiciesAggregation { + indices: { + buckets: Array<{ key: string }>; + }; +} diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts index c8129c2428ee4..201c82060f386 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts @@ -30,7 +30,7 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory ): Promise { service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 8cfd736a336c2..6268f5899d7ff 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -143,11 +143,15 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ params: { body: t.intersection([ t.type({ service: serviceRt }), - t.partial({ etag: t.string }) + t.partial({ etag: t.string, mark_as_applied_by_agent: t.boolean }) ]) }, handler: async ({ context, request }) => { - const { service, etag } = context.params.body; + const { + service, + etag, + mark_as_applied_by_agent: markAsAppliedByAgent + } = context.params.body; const setup = await setupRequest(context, request); const config = await searchConfigurations({ @@ -166,9 +170,14 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ `Config was found for ${service.name}/${service.environment}` ); - // update `applied_by_agent` field if etags match + // update `applied_by_agent` field + // when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags) + // or if etags match. // this happens in the background and doesn't block the response - if (etag === config._source.etag && !config._source.applied_by_agent) { + if ( + (markAsAppliedByAgent || etag === config._source.etag) && + !config._source.applied_by_agent + ) { markAppliedByAgent({ id: config._id, body: config._source, setup }); } diff --git a/x-pack/plugins/apm/server/tutorial/index_pattern.json b/x-pack/plugins/apm/server/tutorial/index_pattern.json index ede9ba54681c0..e0d9595585b88 100644 --- a/x-pack/plugins/apm/server/tutorial/index_pattern.json +++ b/x-pack/plugins/apm/server/tutorial/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index e98b2f52089b3..f6c4fce76f134 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -52,5 +52,5 @@ export interface SpanRaw extends APMBaseDoc { id: string; }; observer?: Observer; - child_ids?: string[]; + child?: { id: string[] }; } diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts index f21a9c25b6e64..285a998030cf6 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts @@ -45,7 +45,6 @@ const customElement: CustomElementPayload = { name: 'MyCustomElement', displayName: 'My Wonderful Custom Element', content: 'This is content', - tags: ['filter', 'graphic'], '@created': '2019-02-08T18:35:23.029Z', '@timestamp': '2019-02-08T18:35:23.029Z', }; diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts new file mode 100644 index 0000000000000..0efdcd3819659 --- /dev/null +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -0,0 +1,28 @@ +/* + * 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 { + CASE_DETAILS_URL, + CASE_COMMENTS_URL, + CASE_USER_ACTIONS_URL, + CASE_COMMENT_DETAILS_URL, +} from '../constants'; + +export const getCaseDetailsUrl = (id: string): string => { + return CASE_DETAILS_URL.replace('{case_id}', id); +}; + +export const getCaseCommentsUrl = (id: string): string => { + return CASE_COMMENTS_URL.replace('{case_id}', id); +}; + +export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): string => { + return CASE_COMMENT_DETAILS_URL.replace('{case_id}', caseId).replace('{comment_id}', commentId); +}; + +export const getCaseUserActionUrl = (id: string): string => { + return CASE_USER_ACTIONS_URL.replace('{case_id}', id); +}; diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts new file mode 100644 index 0000000000000..dcfa46bfa6019 --- /dev/null +++ b/x-pack/plugins/case/common/constants.ts @@ -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. + */ + +export const APP_ID = 'case'; + +/** + * Case routes + */ + +export const CASES_URL = '/api/cases'; +export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`; +export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`; +export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; +export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`; +export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`; +export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`; +export const CASE_STATUS_URL = `${CASES_URL}/status`; +export const CASE_TAGS_URL = `${CASES_URL}/tags`; +export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; + +/** + * Action routes + */ + +export const ACTION_URL = '/api/action'; +export const ACTION_TYPES_URL = '/api/action/types'; diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index 1dfab165eccd7..e9bcb9690ebd8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initDeleteAllCommentsApi({ caseService, router, userActionService }: RouteDeps) { router.delete( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts index b2022e6dec26d..67cb998409570 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts @@ -15,6 +15,7 @@ import { mockCaseComments, } from '../../__fixtures__'; import { initDeleteCommentApi } from './delete_comment'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; describe('DELETE comment', () => { let routeHandler: RequestHandler; @@ -23,7 +24,7 @@ describe('DELETE comment', () => { }); it(`deletes the comment. responds with 204`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'delete', params: { case_id: 'mock-id-1', @@ -43,7 +44,7 @@ describe('DELETE comment', () => { }); it(`returns an error when thrown from deleteComment service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'delete', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index ff0729afed96a..72ef400415d0f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -11,11 +11,12 @@ import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; export function initDeleteCommentApi({ caseService, router, userActionService }: RouteDeps) { router.delete( { - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 92da64cebee74..3df9fdb80ba8a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -18,11 +18,12 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, transformComments, wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/comments/_find', + path: `${CASE_COMMENTS_URL}/_find`, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 1500039eb2cc2..8d7820d4e8fec 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { AllCommentsResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObjects, wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts index 9c8d0e5254df0..b5a7d6367ea4b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts @@ -15,6 +15,7 @@ import { } from '../../__fixtures__'; import { flattenCommentSavedObject } from '../../utils'; import { initGetCommentApi } from './get_comment'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; describe('GET comment', () => { let routeHandler: RequestHandler; @@ -23,7 +24,7 @@ describe('GET comment', () => { }); it(`returns the comment`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', @@ -48,7 +49,7 @@ describe('GET comment', () => { }); it(`returns an error when getComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts index 24f44a5f5129b..5fa668f6ae5de 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { CommentResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; export function initGetCommentApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 8d9906c2abe7f..04473e302e468 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -14,6 +14,7 @@ import { mockCases, } from '../../__fixtures__'; import { initPatchCommentApi } from './patch_comment'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; describe('PATCH comment', () => { let routeHandler: RequestHandler; @@ -22,7 +23,7 @@ describe('PATCH comment', () => { }); it(`Patch a comment`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'patch', params: { case_id: 'mock-id-1', @@ -50,7 +51,7 @@ describe('PATCH comment', () => { it(`Fails with 409 if version does not match`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'patch', params: { case_id: 'mock-id-1', @@ -74,7 +75,7 @@ describe('PATCH comment', () => { }); it(`Returns an error if updateComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'patch', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index 3b38afc02ed81..dd9b124ff1b79 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -15,11 +15,12 @@ import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError, flattenCaseSavedObject } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initPatchCommentApi({ caseService, router, userActionService }: RouteDeps) { router.patch( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 23039da681ec6..9006470f36f36 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -15,6 +15,7 @@ import { mockCaseComments, } from '../../__fixtures__'; import { initPostCommentApi } from './post_comment'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; describe('POST comment', () => { let routeHandler: RequestHandler; @@ -27,7 +28,7 @@ describe('POST comment', () => { }); it(`Posts a new comment`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'mock-id-1', @@ -52,7 +53,7 @@ describe('POST comment', () => { }); it(`Returns an error if the case does not exist`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'this-is-not-real', @@ -75,7 +76,7 @@ describe('POST comment', () => { }); it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'mock-id-1', @@ -100,7 +101,7 @@ describe('POST comment', () => { routeHandler = await createRoute(initPostCommentApi, 'post', true); const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 70405af26f576..a296d9815f251 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -15,11 +15,12 @@ import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { escapeHatch, transformNewComment, wrapError, flattenCaseSavedObject } from '../../utils'; import { RouteDeps } from '../../types'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initPostCommentApi({ caseService, router, userActionService }: RouteDeps) { router.post( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index 66d39c3f11d28..5b3b6e77b9403 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -15,6 +15,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initGetCaseConfigure } from './get_configure'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; describe('GET configuration', () => { let routeHandler: RequestHandler; @@ -24,7 +25,7 @@ describe('GET configuration', () => { it('returns the configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); @@ -44,7 +45,7 @@ describe('GET configuration', () => { it('handles undefined version correctly', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); @@ -78,7 +79,7 @@ describe('GET configuration', () => { it('returns an empty object when there is no configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); @@ -95,7 +96,7 @@ describe('GET configuration', () => { it('returns an error if find throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 2832edaa892d5..03bec1fe72d39 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -7,11 +7,12 @@ import { CaseConfigureResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; export function initGetCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, validate: false, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index 62edaa0a4792a..09692ff73b94b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -16,6 +16,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initCaseConfigureGetActionConnector } from './get_connectors'; import { getActions } from '../../__mocks__/request_responses'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; describe('GET connectors', () => { let routeHandler: RequestHandler; @@ -25,7 +26,7 @@ describe('GET connectors', () => { it('returns the connectors', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure/connectors/_find', + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, method: 'get', }); @@ -44,7 +45,7 @@ describe('GET connectors', () => { it('it throws an error when actions client is null', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure/connectors/_find', + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, method: 'get', }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 3e9a1c96d55ed..00575655d4c42 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -8,6 +8,8 @@ import Boom from 'boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; + /* * Be aware that this api will only return 20 connectors */ @@ -17,7 +19,7 @@ const CASE_SERVICE_NOW_ACTION = '.servicenow'; export function initCaseConfigureGetActionConnector({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/configure/connectors/_find', + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, validate: false, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index 5b3d68a258664..9b71f777b95ab 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -15,6 +15,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPatchCaseConfigure } from './patch_configure'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; describe('PATCH configuration', () => { let routeHandler: RequestHandler; @@ -29,7 +30,7 @@ describe('PATCH configuration', () => { it('patch configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -61,7 +62,7 @@ describe('PATCH configuration', () => { routeHandler = await createRoute(initPatchCaseConfigure, 'patch', true); const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -91,7 +92,7 @@ describe('PATCH configuration', () => { it('throw error when configuration have not being created', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -113,7 +114,7 @@ describe('PATCH configuration', () => { it('throw error when the versions are different', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -135,7 +136,7 @@ describe('PATCH configuration', () => { it('handles undefined version correctly', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { connector_id: 'no-version', version: mockCaseConfigure[0].version }, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 3a1b9d5059cbc..29df97c5f8476 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -16,11 +16,12 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; export function initPatchCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { router.patch( { - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, validate: { body: escapeHatch, }, @@ -38,7 +39,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout if (myCaseConfigure.saved_objects.length === 0) { throw Boom.conflict( - 'You can not patch this configuration since you did not created first with a post' + 'You can not patch this configuration since you did not created first with a post.' ); } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 7e40cad5b1298..fb95cc53a1710 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -16,6 +16,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPostCaseConfigure } from './post_configure'; import { newConfiguration } from '../../__mocks__/request_responses'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; describe('POST configuration', () => { let routeHandler: RequestHandler; @@ -30,7 +31,7 @@ describe('POST configuration', () => { it('create configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -61,7 +62,7 @@ describe('POST configuration', () => { routeHandler = await createRoute(initPostCaseConfigure, 'post', true); const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -90,7 +91,7 @@ describe('POST configuration', () => { it('throws when missing connector_id', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_name: 'My connector 2', @@ -111,7 +112,7 @@ describe('POST configuration', () => { it('throws when missing connector_name', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_id: '456', @@ -132,7 +133,7 @@ describe('POST configuration', () => { it('throws when missing closure_type', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_id: '456', @@ -153,7 +154,7 @@ describe('POST configuration', () => { it('it deletes the previous configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -172,7 +173,7 @@ describe('POST configuration', () => { it('it does NOT delete when not found', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -191,7 +192,7 @@ describe('POST configuration', () => { it('it deletes all configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -214,7 +215,7 @@ describe('POST configuration', () => { it('returns an error if find throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -232,7 +233,7 @@ describe('POST configuration', () => { it('returns an error if delete throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -250,7 +251,7 @@ describe('POST configuration', () => { it('returns an error if post throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_id: 'throw-error-create', @@ -272,7 +273,7 @@ describe('POST configuration', () => { it('handles undefined version correctly', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { ...newConfiguration, connector_id: 'no-version' }, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index 2a23abf0cbf21..5c1693e728c37 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -16,11 +16,12 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; export function initPostCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { router.post( { - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, validate: { body: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts index c5be6f78a1570..e655339e05eb1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts @@ -16,6 +16,7 @@ import { mockCaseComments, } from '../__fixtures__'; import { initDeleteCasesApi } from './delete_cases'; +import { CASES_URL } from '../../../../common/constants'; describe('DELETE case', () => { let routeHandler: RequestHandler; @@ -24,7 +25,7 @@ describe('DELETE case', () => { }); it(`deletes the case. responds with 204`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['mock-id-1'], @@ -43,7 +44,7 @@ describe('DELETE case', () => { }); it(`returns an error when thrown from deleteCase service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['not-real'], @@ -62,7 +63,7 @@ describe('DELETE case', () => { }); it(`returns an error when thrown from getAllCaseComments service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['bad-guy'], @@ -81,7 +82,7 @@ describe('DELETE case', () => { }); it(`returns an error when thrown from deleteComment service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['valid-id'], diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 0214017ae5c29..20591637a6c23 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; +import { CASES_URL } from '../../../../common/constants'; export function initDeleteCasesApi({ caseService, router, userActionService }: RouteDeps) { router.delete( { - path: '/api/cases', + path: CASES_URL, validate: { query: schema.object({ ids: schema.arrayOf(schema.string()), diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts index 8fafb1af0eb82..7af1cee494457 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts @@ -14,6 +14,7 @@ import { mockCases, } from '../__fixtures__'; import { initFindCasesApi } from './find_cases'; +import { CASES_URL } from '../../../../common/constants'; describe('GET all cases', () => { let routeHandler: RequestHandler; @@ -22,7 +23,7 @@ describe('GET all cases', () => { }); it(`gets all the cases`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: `${CASES_URL}/_find`, method: 'get', }); diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts index b2716749e9749..40fc0301b058a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts @@ -15,6 +15,7 @@ import { CasesFindResponseRt, CasesFindRequestRt, throwErrors } from '../../../. import { transformCases, sortToSnake, wrapError, escapeHatch } from '../utils'; import { RouteDeps, TotalCommentByCase } from '../types'; import { CASE_SAVED_OBJECT } from '../../../saved_object_types'; +import { CASES_URL } from '../../../../common/constants'; const combineFilters = (filters: string[], operator: 'OR' | 'AND'): string => filters?.filter(i => i !== '').join(` ${operator} `); @@ -41,7 +42,7 @@ const buildFilter = ( export function initFindCasesApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/_find', + path: `${CASES_URL}/_find`, validate: { query: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 5912df2c40aa3..a8c12d4734b53 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -18,6 +18,7 @@ import { } from '../__fixtures__'; import { flattenCaseSavedObject } from '../utils'; import { initGetCaseApi } from './get_case'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; describe('GET case', () => { let routeHandler: RequestHandler; @@ -26,7 +27,7 @@ describe('GET case', () => { }); it(`returns the case with empty case comments when includeComments is false`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', @@ -55,7 +56,7 @@ describe('GET case', () => { }); it(`returns an error when thrown from getCase`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'abcdefg', @@ -78,7 +79,7 @@ describe('GET case', () => { }); it(`returns the case with case comments when includeComments is true`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', @@ -102,7 +103,7 @@ describe('GET case', () => { }); it(`returns an error when thrown from getAllCaseComments`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'bad-guy', diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index ac32b20541a9c..1e836d38c285c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { CaseResponseRt } from '../../../../common/api'; import { RouteDeps } from '../types'; import { flattenCaseSavedObject, wrapError } from '../utils'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetCaseApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index c36ea8964dc80..57f9fc20dbf34 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -20,11 +20,12 @@ import { escapeHatch, wrapError, flattenCaseSavedObject } from '../utils'; import { RouteDeps } from '../types'; import { getCaseToUpdate } from './helpers'; import { buildCaseUserActions } from '../../../services/user_actions/helpers'; +import { CASES_URL } from '../../../../common/constants'; export function initPatchCasesApi({ caseService, router, userActionService }: RouteDeps) { router.patch( { - path: '/api/cases', + path: CASES_URL, validate: { body: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 5899102224774..0bbceb5214046 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -14,6 +14,7 @@ import { mockCases, } from '../__fixtures__'; import { initPostCaseApi } from './post_case'; +import { CASES_URL } from '../../../../common/constants'; describe('POST cases', () => { let routeHandler: RequestHandler; @@ -26,7 +27,7 @@ describe('POST cases', () => { }); it(`Posts a new case`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'This is a brand new case of a bad meanie defacing data', @@ -49,7 +50,7 @@ describe('POST cases', () => { it(`Error if you passing status for a new case`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'This is a brand new case of a bad meanie defacing data', @@ -70,7 +71,7 @@ describe('POST cases', () => { }); it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'Throw an error', @@ -93,7 +94,7 @@ describe('POST cases', () => { routeHandler = await createRoute(initPostCaseApi, 'post', true); const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'This is a brand new case of a bad meanie defacing data', diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 239b8bfdf9b29..059a8b1affd54 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -14,11 +14,12 @@ import { flattenCaseSavedObject, transformNewCase, wrapError, escapeHatch } from import { CasePostRequestRt, throwErrors, excess, CaseResponseRt } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; +import { CASES_URL } from '../../../../common/constants'; export function initPostCaseApi({ caseService, router, userActionService }: RouteDeps) { router.post( { - path: '/api/cases', + path: CASES_URL, validate: { body: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index aff057adea37f..94ebe24c3d2ae 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -15,6 +15,7 @@ import { flattenCaseSavedObject, wrapError, escapeHatch } from '../utils'; import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initPushCaseUserActionApi({ caseConfigureService, @@ -24,7 +25,7 @@ export function initPushCaseUserActionApi({ }: RouteDeps) { router.post( { - path: '/api/cases/{case_id}/_push', + path: `${CASE_DETAILS_URL}/_push`, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts index 56862a96e0563..3fc96f506d175 100644 --- a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts @@ -7,11 +7,12 @@ import { UsersRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_REPORTERS_URL } from '../../../../../common/constants'; export function initGetReportersApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/reporters', + path: CASE_REPORTERS_URL, validate: {}, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts index f7431729d398c..8f86dbc91f315 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts @@ -9,11 +9,12 @@ import { wrapError } from '../../utils'; import { CasesStatusResponseRt } from '../../../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; +import { CASE_STATUS_URL } from '../../../../../common/constants'; export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/status', + path: CASE_STATUS_URL, validate: {}, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts index 55e8fe2af128c..1a3da659c58c4 100644 --- a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts @@ -6,11 +6,12 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_TAGS_URL } from '../../../../../common/constants'; export function initGetTagsApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/tags', + path: CASE_TAGS_URL, validate: {}, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 2d4f16e46d561..c90979f60d23f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -10,11 +10,12 @@ import { CaseUserActionsResponseRt } from '../../../../../common/api'; import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../../../saved_object_types'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; export function initGetAllUserActionsApi({ userActionService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/user_actions', + path: CASE_USER_ACTIONS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts new file mode 100644 index 0000000000000..797141b0996af --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts @@ -0,0 +1,44 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { LicenseType } from '../../../licensing/common/types'; + +const platinumLicense: LicenseType = 'platinum'; + +export const PLUGIN = { + ID: 'crossClusterReplication', + TITLE: i18n.translate('xpack.crossClusterReplication.appTitle', { + defaultMessage: 'Cross-Cluster Replication', + }), + minimumLicenseType: platinumLicense, +}; + +export const APPS = { + CCR_APP: 'ccr', + REMOTE_CLUSTER_APP: 'remote_cluster', +}; + +export const MANAGEMENT_ID = 'cross_cluster_replication'; +export const BASE_PATH = `/management/elasticsearch/${MANAGEMENT_ID}`; +export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters'; +export const API_BASE_PATH = '/api/cross_cluster_replication'; +export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters'; +export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management'; + +export const FOLLOWER_INDEX_ADVANCED_SETTINGS = { + maxReadRequestOperationCount: 5120, + maxOutstandingReadRequests: 12, + maxReadRequestSize: '32mb', + maxWriteRequestOperationCount: 5120, + maxWriteRequestSize: '9223372036854775807b', + maxOutstandingWriteRequests: 9, + maxWriteBufferCount: 2147483647, + maxWriteBufferSize: '512mb', + maxRetryDelay: '500ms', + readPollTimeout: '1m', +}; diff --git a/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap new file mode 100644 index 0000000000000..c20556fe1434d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = ` +Object { + "leaderIndex": "leader 1", + "maxOutstandingReadRequests": 1, + "maxOutstandingWriteRequests": 1, + "maxReadRequestOperationCount": 1, + "maxReadRequestSize": "1b", + "maxRetryDelay": "1s", + "maxWriteBufferCount": 1, + "maxWriteBufferSize": "1b", + "maxWriteRequestOperationCount": 1, + "maxWriteRequestSize": "1b", + "name": "follower index 1", + "readPollTimeout": "1s", + "remoteCluster": "cluster 1", + "shards": Array [ + Object { + "bytesReadCount": 1, + "failedReadRequestsCount": 1, + "failedWriteRequestsCount": 1, + "followerGlobalCheckpoint": 1, + "followerMappingVersion": 1, + "followerMaxSequenceNum": 1, + "followerSettingsVersion": 1, + "id": 1, + "lastRequestedSequenceNum": 1, + "leaderGlobalCheckpoint": 1, + "leaderIndex": "leader 1", + "leaderMaxSequenceNum": 1, + "operationsReadCount": 1, + "operationsWrittenCount": 1, + "outstandingReadRequestsCount": 1, + "outstandingWriteRequestsCount": 1, + "readExceptions": Array [], + "remoteCluster": "cluster 1", + "successfulReadRequestCount": 1, + "successfulWriteRequestsCount": 1, + "timeSinceLastReadMs": 1, + "totalReadRemoteExecTimeMs": 1, + "totalReadTimeMs": 1, + "totalWriteTimeMs": 1, + "writeBufferOperationsCount": 1, + "writeBufferSizeBytes": 1, + }, + Object { + "bytesReadCount": undefined, + "failedReadRequestsCount": undefined, + "failedWriteRequestsCount": undefined, + "followerGlobalCheckpoint": undefined, + "followerMappingVersion": undefined, + "followerMaxSequenceNum": undefined, + "followerSettingsVersion": undefined, + "id": "shard 2", + "lastRequestedSequenceNum": undefined, + "leaderGlobalCheckpoint": undefined, + "leaderIndex": "leader_index 2", + "leaderMaxSequenceNum": undefined, + "operationsReadCount": undefined, + "operationsWrittenCount": undefined, + "outstandingReadRequestsCount": undefined, + "outstandingWriteRequestsCount": undefined, + "readExceptions": undefined, + "remoteCluster": "remote_cluster 2", + "successfulReadRequestCount": undefined, + "successfulWriteRequestsCount": undefined, + "timeSinceLastReadMs": undefined, + "totalReadRemoteExecTimeMs": undefined, + "totalReadTimeMs": undefined, + "totalWriteTimeMs": undefined, + "writeBufferOperationsCount": undefined, + "writeBufferSizeBytes": undefined, + }, + ], + "status": "active", +} +`; + +exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = ` +Object { + "bytesReadCount": 1, + "failedReadRequestsCount": 1, + "failedWriteRequestsCount": 1, + "followerGlobalCheckpoint": 1, + "followerMappingVersion": 1, + "followerMaxSequenceNum": 1, + "followerSettingsVersion": 1, + "id": 1, + "lastRequestedSequenceNum": 1, + "leaderGlobalCheckpoint": 1, + "leaderIndex": "leader index", + "leaderMaxSequenceNum": 1, + "operationsReadCount": 1, + "operationsWrittenCount": 1, + "outstandingReadRequestsCount": 1, + "outstandingWriteRequestsCount": 1, + "readExceptions": Array [ + "read exception", + ], + "remoteCluster": "remote cluster", + "successfulReadRequestCount": 1, + "successfulWriteRequestsCount": 1, + "timeSinceLastReadMs": 1, + "totalReadRemoteExecTimeMs": 1, + "totalReadTimeMs": 1, + "totalWriteTimeMs": 1, + "writeBufferOperationsCount": 1, + "writeBufferSizeBytes": 1, +} +`; + +exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = ` +Object { + "leader_index": "leader index", + "max_outstanding_read_requests": 1, + "max_outstanding_write_requests": 1, + "max_read_request_operation_count": 1, + "max_read_request_size": "1b", + "max_retry_delay": "1s", + "max_write_buffer_count": 1, + "max_write_buffer_size": "1b", + "max_write_request_operation_count": 1, + "max_write_request_size": "1b", + "read_poll_timeout": "1s", + "remote_cluster": "remote cluster", +} +`; diff --git a/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts new file mode 100644 index 0000000000000..fe3e59f21ee23 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { AutoFollowPattern, AutoFollowPatternFromEs } from '../types'; + +import { + deserializeAutoFollowPattern, + deserializeListAutoFollowPatterns, + serializeAutoFollowPattern, +} from './auto_follow_pattern_serialization'; + +describe('[CCR] auto-follow_serialization', () => { + describe('deserializeAutoFollowPattern()', () => { + it('should deserialize Elasticsearch object', () => { + const expected = { + name: 'some-name', + active: true, + remoteCluster: 'foo', + leaderIndexPatterns: ['foo-*'], + followIndexPattern: 'bar', + }; + + const esObject = { + name: 'some-name', + pattern: { + active: true, + remote_cluster: expected.remoteCluster, + leader_index_patterns: expected.leaderIndexPatterns, + follow_index_pattern: expected.followIndexPattern, + }, + }; + + expect(deserializeAutoFollowPattern(esObject as AutoFollowPatternFromEs)).toEqual(expected); + }); + }); + + describe('deserializeListAutoFollowPatterns()', () => { + it('should deserialize list of Elasticsearch objects', () => { + const name1 = 'foo1'; + const name2 = 'foo2'; + + const expected = [ + { + name: name1, + remoteCluster: 'foo1', + leaderIndexPatterns: ['foo1-*'], + followIndexPattern: 'bar2', + }, + { + name: name2, + remoteCluster: 'foo2', + leaderIndexPatterns: ['foo2-*'], + followIndexPattern: 'bar2', + }, + ]; + + const esObjects = { + patterns: [ + { + name: name1, + pattern: { + remote_cluster: expected[0].remoteCluster, + leader_index_patterns: expected[0].leaderIndexPatterns, + follow_index_pattern: expected[0].followIndexPattern, + }, + }, + { + name: name2, + pattern: { + remote_cluster: expected[1].remoteCluster, + leader_index_patterns: expected[1].leaderIndexPatterns, + follow_index_pattern: expected[1].followIndexPattern, + }, + }, + ], + }; + + expect( + deserializeListAutoFollowPatterns(esObjects.patterns as AutoFollowPatternFromEs[]) + ).toEqual(expected); + }); + }); + + describe('serializeAutoFollowPattern()', () => { + it('should serialize object to Elasticsearch object', () => { + const expected = { + remote_cluster: 'foo', + leader_index_patterns: ['bar-*'], + follow_index_pattern: 'faz', + }; + + const object = { + remoteCluster: expected.remote_cluster, + leaderIndexPatterns: expected.leader_index_patterns, + followIndexPattern: expected.follow_index_pattern, + }; + + expect(serializeAutoFollowPattern(object as AutoFollowPattern)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts new file mode 100644 index 0000000000000..265af0ede1462 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts @@ -0,0 +1,38 @@ +/* + * 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 { AutoFollowPattern, AutoFollowPatternFromEs, AutoFollowPatternToEs } from '../types'; + +export const deserializeAutoFollowPattern = ( + autoFollowPattern: AutoFollowPatternFromEs +): AutoFollowPattern => { + const { + name, + pattern: { active, remote_cluster, leader_index_patterns, follow_index_pattern }, + } = autoFollowPattern; + + return { + name, + active, + remoteCluster: remote_cluster, + leaderIndexPatterns: leader_index_patterns, + followIndexPattern: follow_index_pattern, + }; +}; + +export const deserializeListAutoFollowPatterns = ( + autoFollowPatterns: AutoFollowPatternFromEs[] +): AutoFollowPattern[] => autoFollowPatterns.map(deserializeAutoFollowPattern); + +export const serializeAutoFollowPattern = ({ + remoteCluster, + leaderIndexPatterns, + followIndexPattern, +}: AutoFollowPattern): AutoFollowPatternToEs => ({ + remote_cluster: remoteCluster, + leader_index_patterns: leaderIndexPatterns, + follow_index_pattern: followIndexPattern, +}); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts new file mode 100644 index 0000000000000..bfe3e1b3443e6 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts @@ -0,0 +1,224 @@ +/* + * 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 { ShardFromEs, FollowerIndexFromEs, FollowerIndex } from '../types'; + +import { + deserializeShard, + deserializeFollowerIndex, + deserializeListFollowerIndices, + serializeFollowerIndex, +} from './follower_index_serialization'; + +describe('[CCR] follower index serialization', () => { + describe('deserializeShard()', () => { + it('deserializes shard', () => { + const serializedShard = { + remote_cluster: 'remote cluster', + leader_index: 'leader index', + shard_id: 1, + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: ['read exception'], + time_since_last_read_millis: 1, + }; + + expect(deserializeShard(serializedShard as ShardFromEs)).toMatchSnapshot(); + }); + }); + + describe('deserializeFollowerIndex()', () => { + it('deserializes Elasticsearch follower index object', () => { + const serializedFollowerIndex = { + follower_index: 'follower index 1', + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + shards: [ + { + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + shard_id: 1, + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + // This is an array of exception objects + read_exceptions: [], + time_since_last_read_millis: 1, + }, + { + remote_cluster: 'remote_cluster 2', + leader_index: 'leader_index 2', + shard_id: 'shard 2', + }, + ], + }; + + expect( + deserializeFollowerIndex(serializedFollowerIndex as FollowerIndexFromEs) + ).toMatchSnapshot(); + }); + }); + + describe('deserializeListFollowerIndices()', () => { + it('deserializes list of Elasticsearch follower index objects', () => { + const serializedFollowerIndexList = [ + { + follower_index: 'follower index 1', + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + shards: [], + }, + { + follower_index: 'follower index 2', + remote_cluster: 'cluster 2', + leader_index: 'leader 2', + status: 'paused', + parameters: { + max_read_request_operation_count: 2, + max_outstanding_read_requests: 2, + max_read_request_size: '2b', + max_write_request_operation_count: 2, + max_write_request_size: '2b', + max_outstanding_write_requests: 2, + max_write_buffer_count: 2, + max_write_buffer_size: '2b', + max_retry_delay: '2s', + read_poll_timeout: '2s', + }, + shards: [], + }, + ]; + + const deserializedFollowerIndexList = [ + { + name: 'follower index 1', + remoteCluster: 'cluster 1', + leaderIndex: 'leader 1', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [], + }, + { + name: 'follower index 2', + remoteCluster: 'cluster 2', + leaderIndex: 'leader 2', + status: 'paused', + maxReadRequestOperationCount: 2, + maxOutstandingReadRequests: 2, + maxReadRequestSize: '2b', + maxWriteRequestOperationCount: 2, + maxWriteRequestSize: '2b', + maxOutstandingWriteRequests: 2, + maxWriteBufferCount: 2, + maxWriteBufferSize: '2b', + maxRetryDelay: '2s', + readPollTimeout: '2s', + shards: [], + }, + ]; + + expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual( + deserializedFollowerIndexList + ); + }); + }); + + describe('serializeFollowerIndex()', () => { + it('serializes object to Elasticsearch follower index object', () => { + const deserializedFollowerIndex = { + name: 'test', + status: 'active', + shards: [], + remoteCluster: 'remote cluster', + leaderIndex: 'leader index', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + }; + + expect(serializeFollowerIndex(deserializedFollowerIndex as FollowerIndex)).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts new file mode 100644 index 0000000000000..df476a0b2db89 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts @@ -0,0 +1,142 @@ +/* + * 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 { + ShardFromEs, + Shard, + FollowerIndexFromEs, + FollowerIndex, + FollowerIndexToEs, + FollowerIndexAdvancedSettings, + FollowerIndexAdvancedSettingsToEs, +} from '../types'; + +export const deserializeShard = ({ + remote_cluster, + leader_index, + shard_id, + leader_global_checkpoint, + leader_max_seq_no, + follower_global_checkpoint, + follower_max_seq_no, + last_requested_seq_no, + outstanding_read_requests, + outstanding_write_requests, + write_buffer_operation_count, + write_buffer_size_in_bytes, + follower_mapping_version, + follower_settings_version, + total_read_time_millis, + total_read_remote_exec_time_millis, + successful_read_requests, + failed_read_requests, + operations_read, + bytes_read, + total_write_time_millis, + successful_write_requests, + failed_write_requests, + operations_written, + read_exceptions, + time_since_last_read_millis, +}: ShardFromEs): Shard => ({ + id: shard_id, + remoteCluster: remote_cluster, + leaderIndex: leader_index, + leaderGlobalCheckpoint: leader_global_checkpoint, + leaderMaxSequenceNum: leader_max_seq_no, + followerGlobalCheckpoint: follower_global_checkpoint, + followerMaxSequenceNum: follower_max_seq_no, + lastRequestedSequenceNum: last_requested_seq_no, + outstandingReadRequestsCount: outstanding_read_requests, + outstandingWriteRequestsCount: outstanding_write_requests, + writeBufferOperationsCount: write_buffer_operation_count, + writeBufferSizeBytes: write_buffer_size_in_bytes, + followerMappingVersion: follower_mapping_version, + followerSettingsVersion: follower_settings_version, + totalReadTimeMs: total_read_time_millis, + totalReadRemoteExecTimeMs: total_read_remote_exec_time_millis, + successfulReadRequestCount: successful_read_requests, + failedReadRequestsCount: failed_read_requests, + operationsReadCount: operations_read, + bytesReadCount: bytes_read, + totalWriteTimeMs: total_write_time_millis, + successfulWriteRequestsCount: successful_write_requests, + failedWriteRequestsCount: failed_write_requests, + operationsWrittenCount: operations_written, + // This is an array of exception objects + readExceptions: read_exceptions, + timeSinceLastReadMs: time_since_last_read_millis, +}); + +export const deserializeFollowerIndex = ({ + follower_index, + remote_cluster, + leader_index, + status, + parameters: { + max_read_request_operation_count, + max_outstanding_read_requests, + max_read_request_size, + max_write_request_operation_count, + max_write_request_size, + max_outstanding_write_requests, + max_write_buffer_count, + max_write_buffer_size, + max_retry_delay, + read_poll_timeout, + } = {}, + shards, +}: FollowerIndexFromEs): FollowerIndex => ({ + name: follower_index, + remoteCluster: remote_cluster, + leaderIndex: leader_index, + status, + maxReadRequestOperationCount: max_read_request_operation_count, + maxOutstandingReadRequests: max_outstanding_read_requests, + maxReadRequestSize: max_read_request_size, + maxWriteRequestOperationCount: max_write_request_operation_count, + maxWriteRequestSize: max_write_request_size, + maxOutstandingWriteRequests: max_outstanding_write_requests, + maxWriteBufferCount: max_write_buffer_count, + maxWriteBufferSize: max_write_buffer_size, + maxRetryDelay: max_retry_delay, + readPollTimeout: read_poll_timeout, + shards: shards && shards.map(deserializeShard), +}); + +export const deserializeListFollowerIndices = ( + followerIndices: FollowerIndexFromEs[] +): FollowerIndex[] => followerIndices.map(deserializeFollowerIndex); + +export const serializeAdvancedSettings = ({ + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, +}: FollowerIndexAdvancedSettings): FollowerIndexAdvancedSettingsToEs => ({ + max_read_request_operation_count: maxReadRequestOperationCount, + max_outstanding_read_requests: maxOutstandingReadRequests, + max_read_request_size: maxReadRequestSize, + max_write_request_operation_count: maxWriteRequestOperationCount, + max_write_request_size: maxWriteRequestSize, + max_outstanding_write_requests: maxOutstandingWriteRequests, + max_write_buffer_count: maxWriteBufferCount, + max_write_buffer_size: maxWriteBufferSize, + max_retry_delay: maxRetryDelay, + read_poll_timeout: readPollTimeout, +}); + +export const serializeFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexToEs => ({ + remote_cluster: followerIndex.remoteCluster, + leader_index: followerIndex.leaderIndex, + ...serializeAdvancedSettings(followerIndex), +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js rename to x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts diff --git a/x-pack/plugins/cross_cluster_replication/common/services/utils.ts b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts new file mode 100644 index 0000000000000..dda6732254cc3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const arrify = (val: any): any[] => (Array.isArray(val) ? val : [val]); + +/** + * Utilty to add some latency in a Promise chain + * + * @param {number} time Time in millisecond to wait + */ +export const wait = (time = 1000) => (data: any): Promise => { + return new Promise(resolve => { + setTimeout(() => resolve(data), time); + }); +}; + +/** + * Utility to remove empty fields ("") from a request body + */ +export const removeEmptyFields = (body: Record): Record => + Object.entries(body).reduce((acc: Record, [key, value]: [string, any]): Record< + string, + any + > => { + if (value !== '') { + acc[key] = value; + } + return acc; + }, {}); diff --git a/x-pack/plugins/cross_cluster_replication/common/types.ts b/x-pack/plugins/cross_cluster_replication/common/types.ts new file mode 100644 index 0000000000000..4932d6c570297 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/types.ts @@ -0,0 +1,186 @@ +/* + * 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 AutoFollowPattern { + name: string; + active: boolean; + remoteCluster: string; + leaderIndexPatterns: string[]; + followIndexPattern: string; +} + +export interface AutoFollowPatternFromEs { + name: string; + pattern: { + active: boolean; + remote_cluster: string; + leader_index_patterns: string[]; + follow_index_pattern: string; + }; +} + +export interface AutoFollowPatternToEs { + remote_cluster: string; + leader_index_patterns: string[]; + follow_index_pattern: string; +} + +export interface ShardFromEs { + remote_cluster: string; + leader_index: string; + shard_id: number; + leader_global_checkpoint: number; + leader_max_seq_no: number; + follower_global_checkpoint: number; + follower_max_seq_no: number; + last_requested_seq_no: number; + outstanding_read_requests: number; + outstanding_write_requests: number; + write_buffer_operation_count: number; + write_buffer_size_in_bytes: number; + follower_mapping_version: number; + follower_settings_version: number; + total_read_time_millis: number; + total_read_remote_exec_time_millis: number; + successful_read_requests: number; + failed_read_requests: number; + operations_read: number; + bytes_read: number; + total_write_time_millis: number; + successful_write_requests: number; + failed_write_requests: number; + operations_written: number; + // This is an array of exception objects + read_exceptions: any[]; + time_since_last_read_millis: number; +} + +export interface Shard { + remoteCluster: string; + leaderIndex: string; + id: number; + leaderGlobalCheckpoint: number; + leaderMaxSequenceNum: number; + followerGlobalCheckpoint: number; + followerMaxSequenceNum: number; + lastRequestedSequenceNum: number; + outstandingReadRequestsCount: number; + outstandingWriteRequestsCount: number; + writeBufferOperationsCount: number; + writeBufferSizeBytes: number; + followerMappingVersion: number; + followerSettingsVersion: number; + totalReadTimeMs: number; + totalReadRemoteExecTimeMs: number; + successfulReadRequestCount: number; + failedReadRequestsCount: number; + operationsReadCount: number; + bytesReadCount: number; + totalWriteTimeMs: number; + successfulWriteRequestsCount: number; + failedWriteRequestsCount: number; + operationsWrittenCount: number; + // This is an array of exception objects + readExceptions: any[]; + timeSinceLastReadMs: number; +} + +export interface FollowerIndexFromEs { + follower_index: string; + remote_cluster: string; + leader_index: string; + status: string; + // Once https://github.com/elastic/elasticsearch/issues/54996 is resolved so that paused follower + // indices contain this information, we can removed this optional typing as well as the optional + // typing in FollowerIndexAdvancedSettings and FollowerIndexAdvancedSettingsToEs. + parameters?: FollowerIndexAdvancedSettingsToEs; + shards: ShardFromEs[]; +} + +export interface FollowerIndex extends FollowerIndexAdvancedSettings { + name: string; + remoteCluster: string; + leaderIndex: string; + status: string; + shards: Shard[]; +} + +export interface FollowerIndexToEs extends FollowerIndexAdvancedSettingsToEs { + remote_cluster: string; + leader_index: string; +} + +export interface FollowerIndexAdvancedSettings { + maxReadRequestOperationCount?: number; + maxOutstandingReadRequests?: number; + maxReadRequestSize?: string; // byte value + maxWriteRequestOperationCount?: number; + maxWriteRequestSize?: string; // byte value + maxOutstandingWriteRequests?: number; + maxWriteBufferCount?: number; + maxWriteBufferSize?: string; // byte value + maxRetryDelay?: string; // time value + readPollTimeout?: string; // time value +} + +export interface FollowerIndexAdvancedSettingsToEs { + max_read_request_operation_count?: number; + max_outstanding_read_requests?: number; + max_read_request_size?: string; // byte value + max_write_request_operation_count?: number; + max_write_request_size?: string; // byte value + max_outstanding_write_requests?: number; + max_write_buffer_count?: number; + max_write_buffer_size?: string; // byte value + max_retry_delay?: string; // time value + read_poll_timeout?: string; // time value +} + +export interface RecentAutoFollowError { + timestamp: number; + leaderIndex: string; + autoFollowException: { + type: string; + reason: string; + }; +} + +export interface RecentAutoFollowErrorFromEs { + timestamp: number; + leader_index: string; + auto_follow_exception: { + type: string; + reason: string; + }; +} + +export interface AutoFollowedCluster { + clusterName: string; + timeSinceLastCheckMillis: number; + lastSeenMetadataVersion: number; +} + +export interface AutoFollowedClusterFromEs { + cluster_name: string; + time_since_last_check_millis: number; + last_seen_metadata_version: number; +} + +export interface AutoFollowStats { + numberOfFailedFollowIndices: number; + numberOfFailedRemoteClusterStateRequests: number; + numberOfSuccessfulFollowIndices: number; + recentAutoFollowErrors: RecentAutoFollowError[]; + autoFollowedClusters: AutoFollowedCluster[]; +} + +export interface AutoFollowStatsFromEs { + number_of_failed_follow_indices: number; + number_of_failed_remote_cluster_state_requests: number; + number_of_successful_follow_indices: number; + recent_auto_follow_errors: RecentAutoFollowErrorFromEs[]; + auto_followed_clusters: AutoFollowedClusterFromEs[]; +} diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json new file mode 100644 index 0000000000000..ccf98f41def47 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "crossClusterReplication", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "home", + "licensing", + "management", + "remoteClusters", + "indexManagement" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": ["xpack", "ccr"] +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js index 2be00e70f6f84..db1430d157183 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -3,11 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { indexPatterns } from '../../../../../../src/plugins/data/public'; -jest.mock('ui/new_platform'); +import { indexPatterns } from '../../../../../../src/plugins/data/public'; +import './mocks'; +import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; const { setup } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js index abc3e5dc9def2..170bce7b82085 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { AutoFollowPatternForm } from '../../public/np_ready/app/components/auto_follow_pattern_form'; +import { AutoFollowPatternForm } from '../../app/components/auto_follow_pattern_form'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { AUTO_FOLLOW_PATTERN_EDIT } from './helpers/constants'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.autoFollowPatternEdit; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js index 20e982856dc19..190400e988634 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; +import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { getAutoFollowPatternClientMock } from '../../fixtures/auto_follow_pattern'; - -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.autoFollowPatternList; describe('', () => { @@ -79,11 +76,11 @@ describe('', () => { const testPrefix = 'prefix_'; const testSuffix = '_suffix'; - const autoFollowPattern1 = getAutoFollowPatternClientMock({ + const autoFollowPattern1 = getAutoFollowPatternMock({ name: `a${getRandomString()}`, followIndexPattern: `${testPrefix}{{leader_index}}${testSuffix}`, }); - const autoFollowPattern2 = getAutoFollowPatternClientMock({ + const autoFollowPattern2 = getAutoFollowPatternMock({ name: `b${getRandomString()}`, followIndexPattern: '{{leader_index}}', // no prefix nor suffix }); @@ -305,10 +302,12 @@ describe('', () => { const message = 'bar'; const recentAutoFollowErrors = [ { + timestamp: 1587081600021, leaderIndex: `${autoFollowPattern1.name}:my-leader-test`, autoFollowException: { type: 'exception', reason: message }, }, { + timestamp: 1587081600021, leaderIndex: `${autoFollowPattern2.name}:my-leader-test`, autoFollowException: { type: 'exception', reason: message }, }, @@ -327,7 +326,7 @@ describe('', () => { expect(exists('autoFollowPatternDetail.errors')).toBe(true); expect(exists('autoFollowPatternDetail.titleErrors')).toBe(true); expect(find('autoFollowPatternDetail.recentError').map(error => error.text())).toEqual([ - message, + 'April 16th, 2020 8:00:00 PM: bar', ]); }); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts new file mode 100644 index 0000000000000..e6444c37e8590 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts @@ -0,0 +1,28 @@ +/* + * 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 { getRandomString } from '../../../../../../test_utils'; +import { AutoFollowPattern } from '../../../../common/types'; + +export const getAutoFollowPatternMock = ({ + name = getRandomString(), + active = false, + remoteCluster = getRandomString(), + leaderIndexPatterns = [`${getRandomString()}-*`], + followIndexPattern = getRandomString(), +}: { + name: string; + active: boolean; + remoteCluster: string; + leaderIndexPatterns: string[]; + followIndexPattern: string; +}): AutoFollowPattern => ({ + name, + active, + remoteCluster, + leaderIndexPatterns, + followIndexPattern, +}); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts new file mode 100644 index 0000000000000..ff051d470531b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts @@ -0,0 +1,70 @@ +/* + * 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 { getRandomString } from '../../../../../../test_utils'; +import { FollowerIndex } from '../../../../common/types'; + +const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires +const chance = new Chance(); + +interface FollowerIndexMock { + name: string; + remoteCluster: string; + leaderIndex: string; + status: string; +} + +export const getFollowerIndexMock = ({ + name = getRandomString(), + remoteCluster = getRandomString(), + leaderIndex = getRandomString(), + status = 'Active', +}: FollowerIndexMock): FollowerIndex => ({ + name, + remoteCluster, + leaderIndex, + status, + maxReadRequestOperationCount: chance.integer(), + maxOutstandingReadRequests: chance.integer(), + maxReadRequestSize: getRandomString({ length: 5 }), + maxWriteRequestOperationCount: chance.integer(), + maxWriteRequestSize: '9223372036854775807b', + maxOutstandingWriteRequests: chance.integer(), + maxWriteBufferCount: chance.integer(), + maxWriteBufferSize: getRandomString({ length: 5 }), + maxRetryDelay: getRandomString({ length: 5 }), + readPollTimeout: getRandomString({ length: 5 }), + shards: [ + { + id: 0, + remoteCluster, + leaderIndex, + leaderGlobalCheckpoint: chance.integer(), + leaderMaxSequenceNum: chance.integer(), + followerGlobalCheckpoint: chance.integer(), + followerMaxSequenceNum: chance.integer(), + lastRequestedSequenceNum: chance.integer(), + outstandingReadRequestsCount: chance.integer(), + outstandingWriteRequestsCount: chance.integer(), + writeBufferOperationsCount: chance.integer(), + writeBufferSizeBytes: chance.integer(), + followerMappingVersion: chance.integer(), + followerSettingsVersion: chance.integer(), + totalReadTimeMs: chance.integer(), + totalReadRemoteExecTimeMs: chance.integer(), + successfulReadRequestCount: chance.integer(), + failedReadRequestsCount: chance.integer(), + operationsReadCount: chance.integer(), + bytesReadCount: chance.integer(), + totalWriteTimeMs: chance.integer(), + successfulWriteRequestsCount: chance.integer(), + failedWriteRequestsCount: chance.integer(), + operationsWrittenCount: chance.integer(), + readExceptions: [], + timeSinceLastReadMs: chance.integer(), + }, + ], +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js index 7680be9d858a4..4c99339e16952 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { RemoteClustersFormField } from '../../public/np_ready/app/components'; - import { indexPatterns } from '../../../../../../src/plugins/data/public'; - -jest.mock('ui/new_platform'); +import './mocks'; +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { RemoteClustersFormField } from '../../app/components'; const { setup } = pageHelpers.followerIndexAdd; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js index cfa37ff2e0358..f4bda2af653aa 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { FollowerIndexForm } from '../../public/np_ready/app/components/follower_index_form/follower_index_form'; +import { FollowerIndexForm } from '../../app/components/follower_index_form/follower_index_form'; +import './mocks'; import { FOLLOWER_INDEX_EDIT } from './helpers/constants'; - -jest.mock('ui/new_platform'); +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; const { setup } = pageHelpers.followerIndexEdit; const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index dde31d1d166f9..f98a1dafbbcbf 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getFollowerIndexMock } from './fixtures/follower_index'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { getFollowerIndexMock } from '../../fixtures/follower_index'; - -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.followerIndexList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js similarity index 76% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js index 1f64e589bc4c1..1cb4e7c7725df 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternAdd } from '../../../public/np_ready/app/sections/auto_follow_pattern_add'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternAdd } from '../../../app/sections/auto_follow_pattern_add'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js similarity index 82% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js index 2b110c6552072..9cad61893c409 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternEdit } from '../../../public/np_ready/app/sections/auto_follow_pattern_edit'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternEdit } from '../../../app/sections/auto_follow_pattern_edit'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js index 1d3e8ad6dff83..450feed49f9f2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { AutoFollowPatternList } from '../../../public/np_ready/app/sections/home/auto_follow_pattern_list'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternList } from '../../../app/sections/home/auto_follow_pattern_list'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js similarity index 79% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js index f74baa1b2ad0a..856b09f3f3cba 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexAdd } from '../../../public/np_ready/app/sections/follower_index_add'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndexAdd } from '../../../app/sections/follower_index_add'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js similarity index 84% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js index 47f8539bb593b..893d01f151bc2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexEdit } from '../../../public/np_ready/app/sections/follower_index_edit'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndexEdit } from '../../../app/sections/follower_index_edit'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; import { FOLLOWER_INDEX_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js similarity index 90% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js index 2154e11e17b1f..52f4267594cc1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { FollowerIndicesList } from '../../../public/np_ready/app/sections/home/follower_indices_list'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndicesList } from '../../../app/sections/home/follower_indices_list'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js new file mode 100644 index 0000000000000..56dfa765bfa4f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js @@ -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 { registerTestBed } from '../../../../../../test_utils'; +import { BASE_PATH } from '../../../../common/constants'; +import { CrossClusterReplicationHome } from '../../../app/sections/home/home'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; + +const testBedConfig = { + store: ccrStore, + memoryRouter: { + initialEntries: [`${BASE_PATH}/follower_indices`], + componentRoutePath: `${BASE_PATH}/:section`, + onRouter: router => (routing.reactRouter = router), + }, +}; + +export const setup = registerTestBed(CrossClusterReplicationHome, testBedConfig); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js similarity index 91% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js index 3562ad0df5b51..6dedbbfa79b19 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js @@ -7,7 +7,7 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { setHttpClient } from '../../../public/np_ready/app/services/api'; +import { setHttpClient } from '../../../app/services/api'; import { init as initHttpRequests } from './http_requests'; export const setupEnvironment = () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js similarity index 93% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js index 2c536d069ef53..18d8b4eb9dbe0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.home; describe('', () => { @@ -36,7 +34,7 @@ describe('', () => { ({ exists, find, component } = setup()); }); - test('should set the correct an app title', () => { + test('should set the correct app title', () => { expect(exists('appTitle')).toBe(true); expect(find('appTitle').text()).toEqual('Cross-Cluster Replication'); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts new file mode 100644 index 0000000000000..60a196254d408 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +jest.mock('../../../app/services/breadcrumbs', () => ({ + ...jest.requireActual('../../../app/services/breadcrumbs'), + setBreadcrumbs: jest.fn(), +})); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts new file mode 100644 index 0000000000000..cff9c003f3e80 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './breadcrumbs.mock'; +import './track_ui_metric.mock'; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts new file mode 100644 index 0000000000000..016e259343285 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +jest.mock('../../../app/services/track_ui_metric', () => ({ + ...jest.requireActual('../../../app/services/track_ui_metric'), + trackUiMetric: jest.fn(), + trackUserRequest: (request: Promise) => { + return request.then(response => response); + }, +})); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx new file mode 100644 index 0000000000000..ec349ccd6f2c7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx @@ -0,0 +1,218 @@ +/* + * 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, { Component, Fragment } from 'react'; +import { Route, Switch, Redirect, withRouter, RouteComponentProps } from 'react-router-dom'; +import { History } from 'history'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPageContent, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { BASE_PATH } from '../../common/constants'; +import { getFatalErrors } from './services/notifications'; +import { SectionError } from './components'; +import { routing } from './services/routing'; +// @ts-ignore +import { loadPermissions } from './services/api'; + +// @ts-ignore +import { + CrossClusterReplicationHome, + AutoFollowPatternAdd, + AutoFollowPatternEdit, + FollowerIndexAdd, + FollowerIndexEdit, +} from './sections'; + +interface AppProps { + history: History; + location: any; +} + +interface AppState { + isFetchingPermissions: boolean; + fetchPermissionError: any; + hasPermission: boolean; + missingClusterPrivileges: any[]; +} + +class AppComponent extends Component { + constructor(props: any) { + super(props); + this.registerRouter(); + + this.state = { + isFetchingPermissions: false, + fetchPermissionError: undefined, + hasPermission: false, + missingClusterPrivileges: [], + }; + } + + componentDidMount() { + this.checkPermissions(); + } + + async checkPermissions() { + this.setState({ + isFetchingPermissions: true, + }); + + try { + const { hasPermission, missingClusterPrivileges } = await loadPermissions(); + + this.setState({ + isFetchingPermissions: false, + hasPermission, + missingClusterPrivileges, + }); + } catch (error) { + // Expect an error in the shape provided by Angular's $http service. + if (error && error.body) { + return this.setState({ + isFetchingPermissions: false, + fetchPermissionError: error, + }); + } + + // This error isn't an HTTP error, so let the fatal error screen tell the user something + // unexpected happened. + getFatalErrors().add( + error, + i18n.translate('xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle', { + defaultMessage: 'Cross-Cluster Replication app', + }) + ); + } + } + + registerRouter() { + const { history, location } = this.props; + routing.reactRouter = { + history, + route: { + location, + }, + }; + } + + render() { + const { + isFetchingPermissions, + fetchPermissionError, + hasPermission, + missingClusterPrivileges, + } = this.state; + + if (isFetchingPermissions) { + return ( + + + + + + + + +

+ +

+
+
+
+
+ ); + } + + if (fetchPermissionError) { + return ( + + + } + error={fetchPermissionError} + /> + + + + ); + } + + if (!hasPermission) { + return ( + + + + + } + body={ +

+ +

+ } + /> +
+ ); + } + + return ( +
+ + + + + + + + +
+ ); + } +} + +export const App = withRouter(AppComponent); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap b/x-pack/plugins/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap rename to x-pack/plugins/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx index 12654e56bde97..5474708f313c8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { AutoFollowPatternDeleteProvider } from '../auto_follow_pattern_delete_provider'; // @ts-ignore -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; const actionsAriaLabel = i18n.translate( 'xpack.crossClusterReplication.autoFollowActionMenu.autoFollowPatternActionMenuButtonAriaLabel', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/index.ts b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/index.ts rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.d.ts rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js index 7803b329e6258..f9c03165dcf97 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { deleteAutoFollowPattern } from '../store/actions'; -import { arrify } from '../../../../common/services/utils'; +import { arrify } from '../../../common/services/utils'; class AutoFollowPatternDeleteProviderUi extends PureComponent { state = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js index 5bc5d8ba6e402..c817637ae1854 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js @@ -29,10 +29,10 @@ import { EuiTitle, } from '@elastic/eui'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../src/plugins/data/public'; -import routing from '../services/routing'; +import { routing } from '../services/routing'; import { extractQueryParams } from '../services/query_params'; import { getRemoteClusterName } from '../services/get_remote_cluster_name'; import { API_STATUS } from '../constants'; @@ -512,7 +512,6 @@ export class AutoFollowPatternForm extends PureComponent { - + ( diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts diff --git a/x-pack/plugins/cross_cluster_replication/public/app/index.tsx b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx new file mode 100644 index 0000000000000..79569b587f97f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx @@ -0,0 +1,52 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import { Provider } from 'react-redux'; +import { HashRouter } from 'react-router-dom'; +import { I18nStart } from 'kibana/public'; +import { UnmountCallback } from 'src/core/public'; + +import { init as initBreadcrumbs, SetBreadcrumbs } from './services/breadcrumbs'; +import { init as initDocumentation } from './services/documentation_links'; +import { App } from './app'; +import { ccrStore } from './store'; + +const renderApp = (element: Element, I18nContext: I18nStart['Context']): UnmountCallback => { + render( + + + + + + + , + element + ); + + return () => unmountComponentAtNode(element); +}; + +export async function mountApp({ + element, + setBreadcrumbs, + I18nContext, + ELASTIC_WEBSITE_URL, + DOC_LINK_VERSION, +}: { + element: Element; + setBreadcrumbs: SetBreadcrumbs; + I18nContext: I18nStart['Context']; + ELASTIC_WEBSITE_URL: string; + DOC_LINK_VERSION: string; +}): Promise { + // Import and initialize additional services here instead of in plugin.ts to reduce the size of the + // initial bundle as much as possible. + initBreadcrumbs(setBreadcrumbs); + initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`); + + return renderApp(element, I18nContext); +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js similarity index 80% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js index 2c90456076f85..be470edc07537 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js @@ -39,8 +39,23 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ getAutoFollowPattern: id => dispatch(getAutoFollowPattern(id)), selectAutoFollowPattern: id => dispatch(selectEditAutoFollowPattern(id)), - saveAutoFollowPattern: (id, autoFollowPattern) => - dispatch(saveAutoFollowPattern(id, autoFollowPattern, true)), + saveAutoFollowPattern: (id, autoFollowPattern) => { + // Strip out errors. + const { active, remoteCluster, leaderIndexPatterns, followIndexPattern } = autoFollowPattern; + + dispatch( + saveAutoFollowPattern( + id, + { + active, + remoteCluster, + leaderIndexPatterns, + followIndexPattern, + }, + true + ) + ); + }, clearApiError: () => { dispatch(clearApiError(`${scope}-get`)); dispatch(clearApiError(`${scope}-save`)); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js index 4cd3617abd989..387d7817a0357 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui'; import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { AutoFollowPatternForm, AutoFollowPatternPageTitle, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js index 21493602c12a7..22f9a7338384b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { FollowerIndexForm, FollowerIndexPageTitle, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js index e9e14f57e814f..c8cf94842aa68 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import routing from '../../../services/routing'; +import { routing } from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 956a9f10d810b..eb90e59e99fee 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -20,8 +20,8 @@ import { AutoFollowPatternDeleteProvider, AutoFollowPatternActionMenu, } from '../../../../../components'; -import routing from '../../../../../services/routing'; -import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; +import { routing } from '../../../../../services/routing'; +import { trackUiMetric } from '../../../../../services/track_ui_metric'; export class AutoFollowPatternTable extends PureComponent { static propTypes = { @@ -86,7 +86,7 @@ export class AutoFollowPatternTable extends PureComponent { return ( { - trackUiMetric(METRIC_TYPE.CLICK, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); selectAutoFollowPattern(name); }} data-test-subj="autoFollowPatternLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js new file mode 100644 index 0000000000000..3f2ed82420ff1 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js @@ -0,0 +1,373 @@ +/* + * 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, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; + +import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public'; + +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiHealth, + EuiIcon, + EuiLink, + EuiSpacer, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; + +import { + AutoFollowPatternIndicesPreview, + AutoFollowPatternActionMenu, +} from '../../../../../components'; + +export class DetailPanel extends Component { + static propTypes = { + apiStatus: PropTypes.string, + autoFollowPatternId: PropTypes.string, + autoFollowPattern: PropTypes.object, + closeDetailPanel: PropTypes.func.isRequired, + }; + + renderAutoFollowPattern({ + followIndexPatternPrefix, + followIndexPatternSuffix, + remoteCluster, + leaderIndexPatterns, + active, + }) { + return ( + <> +
+ + + + + + + + + {!active ? ( + + + + ) : ( + + + + )} + + +
+ + +
+ +

+ +

+
+ + + + + + + + + + + + + + {remoteCluster} + + + + + + + + + + + + {leaderIndexPatterns.join(', ')} + + + + + + + + + + + + + + + + {followIndexPatternPrefix || ( + + + + )} + + + + + + + + + + + + {followIndexPatternSuffix || ( + + + + )} + + + + +
+ + ); + } + + renderIndicesPreview(prefix, suffix, leaderIndexPatterns) { + return ( +
+ +
+ ); + } + + renderAutoFollowPatternNotFound() { + return ( + + + + + + + + + + + + + + + + ); + } + + renderAutoFollowPatternErrors(autoFollowPattern) { + if (!autoFollowPattern.errors.length) { + return null; + } + + return ( +
+ + + + + + + +

+ +

+
+
+
+ + +
    + {autoFollowPattern.errors.map((error, i) => ( +
  • + {moment(error.timestamp).format('MMMM Do, YYYY h:mm:ss A')}:{' '} + {error.autoFollowException.reason} +
  • + ))} +
+
+
+ ); + } + + renderFlyoutBody() { + const { autoFollowPattern } = this.props; + + if (!autoFollowPattern) { + return this.renderAutoFollowPatternNotFound(); + } + + const { + followIndexPatternPrefix, + followIndexPatternSuffix, + leaderIndexPatterns, + } = autoFollowPattern; + + let indexManagementFilter; + + if (followIndexPatternPrefix) { + indexManagementFilter = `name:${followIndexPatternPrefix}`; + } else if (followIndexPatternSuffix) { + indexManagementFilter = `name:${followIndexPatternSuffix}`; + } + + const indexManagementUri = getIndexListUri(indexManagementFilter); + + return ( + + {this.renderAutoFollowPattern(autoFollowPattern)} + + + + {this.renderIndicesPreview( + followIndexPatternPrefix, + followIndexPatternSuffix, + leaderIndexPatterns + )} + + + + + + + + + + {this.renderAutoFollowPatternErrors(autoFollowPattern)} + + ); + } + + renderFlyoutFooter() { + const { autoFollowPattern, closeDetailPanel } = this.props; + + return ( + + + + + + + + + {autoFollowPattern && ( + + + + + + + + )} + + + ); + } + + render() { + const { autoFollowPatternId, closeDetailPanel } = this.props; + + return ( + + + +

{autoFollowPatternId}

+
+
+ + {this.renderFlyoutBody()} + {this.renderFlyoutFooter()} +
+ ); + } +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js index 0f6ef75522ff7..4a66f7b717bac 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js @@ -15,7 +15,7 @@ import { EuiPopoverTitle, } from '@elastic/eui'; -import routing from '../../../../../services/routing'; +import { routing } from '../../../../../services/routing'; import { FollowerIndexPauseProvider, FollowerIndexResumeProvider, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js new file mode 100644 index 0000000000000..4436d76643e6c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js @@ -0,0 +1,509 @@ +/* + * 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, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCodeEditor, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiHealth, + EuiIcon, + EuiLoadingSpinner, + EuiSpacer, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import 'brace/theme/textmate'; + +import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public'; +import { API_STATUS } from '../../../../../constants'; +import { ContextMenu } from '../context_menu'; + +export class DetailPanel extends Component { + static propTypes = { + apiStatus: PropTypes.string, + followerIndexId: PropTypes.string, + followerIndex: PropTypes.object, + closeDetailPanel: PropTypes.func.isRequired, + }; + + renderFollowerIndex() { + const { + followerIndex: { + remoteCluster, + leaderIndex, + isPaused, + shards, + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, + }, + } = this.props; + + return ( + + +
+ + + + + + + + + + + {isPaused ? ( + + + + ) : ( + + + + )} + + + + + + + + + + + + + + + + {remoteCluster} + + + + + + + + + + + + {leaderIndex} + + + + +
+ + + +
+ +

+ +

+
+ + + + {isPaused ? ( + + } + /> + ) : ( + + + + + + + + + + + {maxReadRequestOperationCount} + + + + + + + + + + + + {maxOutstandingReadRequests} + + + + + + + + + + + + + + + + {maxReadRequestSize} + + + + + + + + + + + + {maxWriteRequestOperationCount} + + + + + + + + + + + + + + + + {maxWriteRequestSize} + + + + + + + + + + + + {maxOutstandingWriteRequests} + + + + + + + + + + + + + + + + {maxWriteBufferCount} + + + + + + + + + + + + {maxWriteBufferSize} + + + + + + + + + + + + + + + + {maxRetryDelay} + + + + + + + + + + + + {readPollTimeout} + + + + + )} +
+ + + +
+ {shards && + shards.map((shard, i) => ( + + + +

+ +

+
+ + +
+ ))} +
+
+
+ ); + } + + renderContent() { + const { apiStatus, followerIndex } = this.props; + + if (apiStatus === API_STATUS.LOADING) { + return ( + + + + + + + + + + + + + + + + ); + } + + if (!followerIndex) { + return ( + + + + + + + + + + + + + + + + ); + } + + return this.renderFollowerIndex(); + } + + renderFooter() { + const { followerIndexId, followerIndex, closeDetailPanel } = this.props; + + // Use ID instead of followerIndex, because followerIndex may not be loaded yet. + const indexManagementUri = getIndexListUri(`name:${followerIndexId}`); + + return ( + + + + + + + + + + + + + + + + + {followerIndex && ( + + + } + followerIndices={[followerIndex]} + testSubj="manageButton" + /> + + )} + + + + + ); + } + + render() { + const { followerIndexId, closeDetailPanel } = this.props; + + return ( + + + +

{followerIndexId}

+
+
+ + {this.renderContent()} + {this.renderFooter()} +
+ ); + } +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 60feac5ec8c6e..67f42fb622bf8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -22,8 +22,8 @@ import { FollowerIndexResumeProvider, FollowerIndexUnfollowProvider, } from '../../../../../components'; -import routing from '../../../../../services/routing'; -import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; +import { routing } from '../../../../../services/routing'; +import { trackUiMetric } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; export class FollowerIndicesTable extends PureComponent { @@ -171,7 +171,7 @@ export class FollowerIndicesTable extends PureComponent { return ( { - trackUiMetric(METRIC_TYPE.CLICK, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); selectFollowerIndex(name); }} data-test-subj="followerIndexLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index b7e04721f4748..7b843d08cefd3 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import routing from '../../../services/routing'; +import { routing } from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js similarity index 96% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js index 88db909612245..bcd9dad114862 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js @@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; -import { BASE_PATH } from '../../../../../common/constants'; +import { BASE_PATH } from '../../../../common/constants'; import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { AutoFollowPatternList } from './auto_follow_pattern_list'; import { FollowerIndicesList } from './follower_indices_list'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts new file mode 100644 index 0000000000000..b7c1f495604be --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.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 declare const CrossClusterReplicationHome: any; +export declare const AutoFollowPatternAdd: any; +export declare const AutoFollowPatternEdit: any; +export declare const FollowerIndexAdd: any; +export declare const FollowerIndexEdit: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap b/x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap rename to x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/api.js index 24bc7e17356e2..adff40ef29be6 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js @@ -7,8 +7,8 @@ import { API_BASE_PATH, API_REMOTE_CLUSTERS_BASE_PATH, API_INDEX_MANAGEMENT_BASE_PATH, -} from '../../../../common/constants'; -import { arrify } from '../../../../common/services/utils'; +} from '../../../common/constants'; +import { arrify } from '../../../common/services/utils'; import { UIM_FOLLOWER_INDEX_CREATE, UIM_FOLLOWER_INDEX_UPDATE, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js index 95aa3f0ebc3e4..70311d5ba1e4d 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js @@ -9,11 +9,12 @@ export const parseAutoFollowError = error => { return null; } - const { leaderIndex, autoFollowException } = error; + const { leaderIndex, autoFollowException, timestamp } = error; const id = leaderIndex.substring(0, leaderIndex.lastIndexOf(':')); return { id, + timestamp, leaderIndex, autoFollowException, }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js index 1b5a39658ee46..cf394d4b3c7d8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js @@ -8,8 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../src/plugins/data/public'; const { indexNameBeginsWithPeriod, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts new file mode 100644 index 0000000000000..84ac9356462ad --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts @@ -0,0 +1,41 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ChromeBreadcrumb } from 'src/core/public'; + +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; + +import { BASE_PATH } from '../../../common/constants'; + +export type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + +let setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + +export const init = (_setBreadcrumbs: SetBreadcrumbs): void => { + setBreadcrumbs = _setBreadcrumbs; +}; + +export const listBreadcrumb = { + text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', { + defaultMessage: 'Cross-Cluster Replication', + }), + href: `#${BASE_PATH}`, +}; + +export const addBreadcrumb = { + text: i18n.translate('xpack.crossClusterReplication.addBreadcrumbTitle', { + defaultMessage: 'Add', + }), +}; + +export const editBreadcrumb = { + text: i18n.translate('xpack.crossClusterReplication.editBreadcrumbTitle', { + defaultMessage: 'Edit', + }), +}; + +export { setBreadcrumbs }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts new file mode 100644 index 0000000000000..c8b00f6e246b5 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +let _esBase: string; + +export const init = (esBase: string) => { + _esBase = esBase; +}; + +export const getAutoFollowPatternUrl = (): string => `${_esBase}/ccr-put-auto-follow-pattern.html`; +export const getFollowerIndexUrl = (): string => `${_esBase}/ccr-put-follow.html`; +export const getByteUnitsUrl = (): string => `${_esBase}/common-options.html#byte-units`; +export const getTimeUnitsUrl = (): string => `${_esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js similarity index 89% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js index d20fa76ef5451..118a54887d404 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../common/constants'; +import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../common/constants'; export const getSettingDefault = name => { if (!FOLLOWER_INDEX_ADVANCED_SETTINGS[name]) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js b/x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js index 64c3e8412437e..7e2b45b625c1f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; const isEmpty = value => { return !value || !value.trim().length; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts new file mode 100644 index 0000000000000..66fc9de00995c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IToasts, FatalErrorsSetup } from 'src/core/public'; + +let _toasts: IToasts; +let _fatalErrors: FatalErrorsSetup; + +export const init = (toasts: IToasts, fatalErrors: FatalErrorsSetup) => { + _toasts = toasts; + _fatalErrors = fatalErrors; +}; + +export const getToasts = () => _toasts; +export const getFatalErrors = () => _fatalErrors; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js b/x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts new file mode 100644 index 0000000000000..9e96ea12856f6 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts @@ -0,0 +1,7 @@ +/* + * 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 declare const routing: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js similarity index 93% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/routing.js index 965aeaaad22ad..124c61e1ba19e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js @@ -10,7 +10,7 @@ import { createLocation } from 'history'; import { stringify } from 'query-string'; -import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants'; +import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; const isModifiedEvent = event => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); @@ -32,7 +32,6 @@ const appToBasePathMap = { }; class Routing { - _userHasLeftApp = false; _reactRouter = null; /** @@ -97,14 +96,6 @@ class Routing { set reactRouter(router) { this._reactRouter = router; } - - get userHasLeftApp() { - return this._userHasLeftApp; - } - - set userHasLeftApp(hasLeft) { - this._userHasLeftApp = hasLeft; - } } -export default new Routing(); +export const routing = new Routing(); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts new file mode 100644 index 0000000000000..aecc4eb83893f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts @@ -0,0 +1,37 @@ +/* + * 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics'; + +import { UIM_APP_NAME } from '../constants'; + +export { METRIC_TYPE }; + +// usageCollection is an optional dependency, so we default to a no-op. +export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; + +export function init(usageCollection: UsageCollectionSetup): void { + trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME); +} + +/** + * Transparently return provided request Promise, while allowing us to track + * a successful completion of the request. + */ +export function trackUserRequest(request: Promise, actionType: string) { + // Only track successful actions. + return request.then(response => { + // It looks like we're using the wrong type here, added via + // https://github.com/elastic/kibana/pull/41113/files#diff-e65a0a6696a9d723969afd871cbd60cdR19 + // but we'll keep it for now to avoid discontinuity in our telemetry data. + trackUiMetric(METRIC_TYPE.LOADED, actionType); + + // We return the response immediately without waiting for the tracking request to resolve, + // to avoid adding additional latency. + return response; + }); +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js b/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js new file mode 100644 index 0000000000000..52a22cb17d0a9 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js @@ -0,0 +1,257 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { getToasts } from '../../services/notifications'; +import { SECTIONS, API_STATUS } from '../../constants'; +import { + loadAutoFollowPatterns as loadAutoFollowPatternsRequest, + getAutoFollowPattern as getAutoFollowPatternRequest, + createAutoFollowPattern as createAutoFollowPatternRequest, + updateAutoFollowPattern as updateAutoFollowPatternRequest, + deleteAutoFollowPattern as deleteAutoFollowPatternRequest, + pauseAutoFollowPattern as pauseAutoFollowPatternRequest, + resumeAutoFollowPattern as resumeAutoFollowPatternRequest, +} from '../../services/api'; +import { routing } from '../../services/routing'; +import * as t from '../action_types'; +import { sendApiRequest } from './api'; +import { getSelectedAutoFollowPatternId } from '../selectors'; + +const { AUTO_FOLLOW_PATTERN: scope } = SECTIONS; + +export const selectDetailAutoFollowPattern = id => ({ + type: t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL, + payload: id, +}); + +export const selectEditAutoFollowPattern = id => ({ + type: t.AUTO_FOLLOW_PATTERN_SELECT_EDIT, + payload: id, +}); + +export const loadAutoFollowPatterns = (isUpdating = false) => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_LOAD, + scope, + status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, + handler: async () => await loadAutoFollowPatternsRequest(), + }); + +export const getAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_GET, + scope: `${scope}-get`, + handler: async () => await getAutoFollowPatternRequest(id), + }); + +export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) => + sendApiRequest({ + label: isUpdating ? t.AUTO_FOLLOW_PATTERN_UPDATE : t.AUTO_FOLLOW_PATTERN_CREATE, + status: API_STATUS.SAVING, + scope: `${scope}-save`, + handler: async () => { + if (isUpdating) { + return await updateAutoFollowPatternRequest(id, autoFollowPattern); + } + return await createAutoFollowPatternRequest({ id, ...autoFollowPattern }); + }, + onSuccess() { + const successMessage = isUpdating + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.updateAction.successNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' updated successfully`, + values: { name: id }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.addAction.successNotificationTitle', + { + defaultMessage: `Added auto-follow pattern '{name}'`, + values: { name: id }, + } + ); + + getToasts().addSuccess(successMessage); + routing.navigate(`/auto_follow_patterns`, undefined, { + pattern: encodeURIComponent(id), + }); + }, + }); + +export const deleteAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_DELETE, + scope: `${scope}-delete`, + status: API_STATUS.DELETING, + handler: async () => deleteAutoFollowPatternRequest(id), + onSuccess(response, dispatch, getState) { + /** + * We can have 1 or more auto-follow pattern delete operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error removing {count} auto-follow patterns`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorSingleNotificationTitle', + { + defaultMessage: `Error removing the '{name}' auto-follow pattern`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsDeleted.length) { + const hasMultipleDelete = response.itemsDeleted.length > 1; + + const successMessage = hasMultipleDelete + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} auto-follow patterns were removed`, + values: { count: response.itemsDeleted.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successSingleNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' was removed`, + values: { name: response.itemsDeleted[0] }, + } + ); + + getToasts().addSuccess(successMessage); + + // If we've just deleted a pattern we were looking at, we need to close the panel. + const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState()); + if (response.itemsDeleted.includes(autoFollowPatternId)) { + dispatch(selectDetailAutoFollowPattern(null)); + } + } + }, + }); + +export const pauseAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_PAUSE, + scope: `${scope}-pause`, + status: API_STATUS.UPDATING, + handler: () => pauseAutoFollowPatternRequest(id), + onSuccess: response => { + /** + * We can have 1 or more auto-follow pattern pause operations + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error pausing {count} auto-follow patterns`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorSingleNotificationTitle', + { + defaultMessage: `Error pausing the '{name}' auto-follow pattern`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsPaused.length) { + const hasMultiple = response.itemsPaused.length > 1; + + const successMessage = hasMultiple + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} auto-follow patterns were paused`, + values: { count: response.itemsPaused.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successSingleNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' was paused`, + values: { name: response.itemsPaused[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + }, + }); + +export const resumeAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_RESUME, + scope: `${scope}-resume`, + status: API_STATUS.UPDATING, + handler: () => resumeAutoFollowPatternRequest(id), + onSuccess: response => { + /** + * We can have 1 or more auto-follow pattern resume operations + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error resuming {count} auto-follow patterns`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorSingleNotificationTitle', + { + defaultMessage: `Error resuming the '{name}' auto-follow pattern`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsResumed.length) { + const hasMultiple = response.itemsResumed.length > 1; + + const successMessage = hasMultiple + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} auto-follow patterns were resumed`, + values: { count: response.itemsResumed.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successSingleNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' was resumed`, + values: { name: response.itemsResumed[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + }, + }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js new file mode 100644 index 0000000000000..d081e0444eb58 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js @@ -0,0 +1,287 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { routing } from '../../services/routing'; +import { getToasts } from '../../services/notifications'; +import { SECTIONS, API_STATUS } from '../../constants'; +import { + loadFollowerIndices as loadFollowerIndicesRequest, + getFollowerIndex as getFollowerIndexRequest, + createFollowerIndex as createFollowerIndexRequest, + pauseFollowerIndex as pauseFollowerIndexRequest, + resumeFollowerIndex as resumeFollowerIndexRequest, + unfollowLeaderIndex as unfollowLeaderIndexRequest, + updateFollowerIndex as updateFollowerIndexRequest, +} from '../../services/api'; +import * as t from '../action_types'; +import { sendApiRequest } from './api'; +import { getSelectedFollowerIndexId } from '../selectors'; + +const { FOLLOWER_INDEX: scope } = SECTIONS; + +export const selectDetailFollowerIndex = id => ({ + type: t.FOLLOWER_INDEX_SELECT_DETAIL, + payload: id, +}); + +export const selectEditFollowerIndex = id => ({ + type: t.FOLLOWER_INDEX_SELECT_EDIT, + payload: id, +}); + +export const loadFollowerIndices = (isUpdating = false) => + sendApiRequest({ + label: t.FOLLOWER_INDEX_LOAD, + scope, + status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, + handler: async () => await loadFollowerIndicesRequest(), + }); + +export const getFollowerIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_GET, + scope: `${scope}-get`, + handler: async () => await getFollowerIndexRequest(id), + }); + +export const saveFollowerIndex = (name, followerIndex, isUpdating = false) => + sendApiRequest({ + label: t.FOLLOWER_INDEX_CREATE, + status: API_STATUS.SAVING, + scope: `${scope}-save`, + handler: async () => { + if (isUpdating) { + return await updateFollowerIndexRequest(name, followerIndex); + } + return await createFollowerIndexRequest({ name, ...followerIndex }); + }, + onSuccess() { + const successMessage = isUpdating + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.updateAction.successNotificationTitle', + { + defaultMessage: `Follower index '{name}' updated successfully`, + values: { name }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.addAction.successNotificationTitle', + { + defaultMessage: `Added follower index '{name}'`, + values: { name }, + } + ); + + getToasts().addSuccess(successMessage); + routing.navigate(`/follower_indices`, undefined, { + name: encodeURIComponent(name), + }); + }, + }); + +export const pauseFollowerIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_PAUSE, + status: API_STATUS.SAVING, + scope, + handler: async () => pauseFollowerIndexRequest(id), + onSuccess(response, dispatch) { + /** + * We can have 1 or more follower index pause operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error pausing {count} follower indices`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.errorSingleNotificationTitle', + { + defaultMessage: `Error pausing follower index '{name}'`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsPaused.length) { + const hasMultiplePaused = response.itemsPaused.length > 1; + + const successMessage = hasMultiplePaused + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} follower indices were paused`, + values: { count: response.itemsPaused.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.successSingleNotificationTitle', + { + defaultMessage: `Follower index '{name}' was paused`, + values: { name: response.itemsPaused[0] }, + } + ); + + getToasts().addSuccess(successMessage); + + // Refresh list + dispatch(loadFollowerIndices(true)); + } + }, + }); + +export const resumeFollowerIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_RESUME, + status: API_STATUS.SAVING, + scope, + handler: async () => resumeFollowerIndexRequest(id), + onSuccess(response, dispatch) { + console.log('response', response); + /** + * We can have 1 or more follower index resume operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error resuming {count} follower indices`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.errorSingleNotificationTitle', + { + defaultMessage: `Error resuming follower index '{name}'`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsResumed.length) { + const hasMultipleResumed = response.itemsResumed.length > 1; + + const successMessage = hasMultipleResumed + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} follower indices were resumed`, + values: { count: response.itemsResumed.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.successSingleNotificationTitle', + { + defaultMessage: `Follower index '{name}' was resumed`, + values: { name: response.itemsResumed[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + + // Refresh list + dispatch(loadFollowerIndices(true)); + }, + }); + +export const unfollowLeaderIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_UNFOLLOW, + status: API_STATUS.DELETING, + scope: `${scope}-delete`, + handler: async () => unfollowLeaderIndexRequest(id), + onSuccess(response, dispatch, getState) { + /** + * We can have 1 or more follower index unfollow operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error unfollowing leader index of {count} follower indices`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorSingleNotificationTitle', + { + defaultMessage: `Error unfollowing leader index of follower index '{name}'`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsUnfollowed.length) { + const hasMultipleUnfollow = response.itemsUnfollowed.length > 1; + + const successMessage = hasMultipleUnfollow + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.successMultipleNotificationTitle', + { + defaultMessage: `Leader indices of {count} follower indices were unfollowed`, + values: { count: response.itemsUnfollowed.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.successSingleNotificationTitle', + { + defaultMessage: `Leader index of follower index '{name}' was unfollowed`, + values: { name: response.itemsUnfollowed[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + + if (response.itemsNotOpen.length) { + const hasMultipleNotOpen = response.itemsNotOpen.length > 1; + + const warningMessage = hasMultipleNotOpen + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningMultipleNotificationTitle', + { + defaultMessage: `{count} indices could not be re-opened`, + values: { count: response.itemsNotOpen.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningSingleNotificationTitle', + { + defaultMessage: `Index '{name}' could not be re-opened`, + values: { name: response.itemsNotOpen[0] }, + } + ); + + getToasts().addWarning(warningMessage); + } + + // If we've just unfollowed a follower index we were looking at, we need to close the panel. + const followerIndexId = getSelectedFollowerIndexId('detail')(getState()); + if (response.itemsUnfollowed.includes(followerIndexId)) { + dispatch(selectDetailFollowerIndex(null)); + } + }, + }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts new file mode 100644 index 0000000000000..6d35dfeddfd46 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts @@ -0,0 +1,7 @@ +/* + * 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 declare const ccrStore: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js b/x-pack/plugins/cross_cluster_replication/public/app/store/store.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/store.js diff --git a/x-pack/plugins/cross_cluster_replication/public/index.ts b/x-pack/plugins/cross_cluster_replication/public/index.ts new file mode 100644 index 0000000000000..e3e2d860e526d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/public'; + +import { CrossClusterReplicationPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new CrossClusterReplicationPlugin(initializerContext); diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts new file mode 100644 index 0000000000000..bdaa04e9d53ee --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -0,0 +1,102 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; +import { first } from 'rxjs/operators'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; + +import { PLUGIN, MANAGEMENT_ID } from '../common/constants'; +import { init as initUiMetric } from './app/services/track_ui_metric'; +import { init as initNotification } from './app/services/notifications'; +import { PluginDependencies, ClientConfigType } from './types'; + +// @ts-ignore; +import { setHttpClient } from './app/services/api'; + +export class CrossClusterReplicationPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(coreSetup: CoreSetup, plugins: PluginDependencies) { + const { licensing, remoteClusters, usageCollection, management, indexManagement } = plugins; + const esSection = management.sections.getSection('elasticsearch'); + + const { + http, + notifications: { toasts }, + fatalErrors, + getStartServices, + } = coreSetup; + + // Initialize services even if the app isn't mounted, because they're used by index management extensions. + setHttpClient(http); + initUiMetric(usageCollection); + initNotification(toasts, fatalErrors); + + const ccrApp = esSection!.registerApp({ + id: MANAGEMENT_ID, + title: PLUGIN.TITLE, + order: 4, + mount: async ({ element, setBreadcrumbs }) => { + const { mountApp } = await import('./app'); + + const [coreStart] = await getStartServices(); + const { + i18n: { Context: I18nContext }, + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + } = coreStart; + + return mountApp({ + element, + setBreadcrumbs, + I18nContext, + ELASTIC_WEBSITE_URL, + DOC_LINK_VERSION, + }); + }, + }); + + ccrApp.disable(); + + licensing.license$ + .pipe(first()) + .toPromise() + .then(license => { + const licenseStatus = license.check(PLUGIN.ID, PLUGIN.minimumLicenseType); + const isLicenseOk = licenseStatus.state === 'valid'; + const config = this.initializerContext.config.get(); + + // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting. + // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if + // the Remote Clusters UI is disabled we can't show the CCR UI. + const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; + + if (isLicenseOk && isCcrUiEnabled) { + ccrApp.enable(); + + if (indexManagement) { + const propertyPath = 'isFollowerIndex'; + + const followerBadgeExtension = { + matchIndex: (index: any) => { + return get(index, propertyPath); + }, + label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', { + defaultMessage: 'Follower', + }), + color: 'default', + filterExpression: 'isFollowerIndex:true', + }; + + indexManagement.extensionsService.addBadge(followerBadgeExtension); + } + } + }); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/cross_cluster_replication/public/types.ts b/x-pack/plugins/cross_cluster_replication/public/types.ts new file mode 100644 index 0000000000000..aac174b7524d3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/types.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 { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { IndexManagementPluginSetup } from '../../index_management/public'; +import { RemoteClustersPluginSetup } from '../../remote_clusters/public'; +import { LicensingPluginSetup } from '../../licensing/public'; + +export interface PluginDependencies { + usageCollection: UsageCollectionSetup; + management: ManagementSetup; + indexManagement: IndexManagementPluginSetup; + remoteClusters: RemoteClustersPluginSetup; + licensing: LicensingPluginSetup; +} + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts new file mode 100644 index 0000000000000..d4de54391286b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts @@ -0,0 +1,198 @@ +/* + * 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 const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { + const ca = components.clientAction.factory; + + Client.prototype.ccr = components.clientAction.namespaceFactory(); + const ccr = Client.prototype.ccr.prototype; + + ccr.permissions = ca({ + urls: [ + { + fmt: '/_security/user/_has_privileges', + }, + ], + needBody: true, + method: 'POST', + }); + + ccr.autoFollowPatterns = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow', + }, + ], + method: 'GET', + }); + + ccr.autoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ccr.saveAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>', + req: { + id: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'PUT', + }); + + ccr.deleteAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>', + req: { + id: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'DELETE', + }); + + ccr.pauseAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>/pause', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); + + ccr.resumeAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>/resume', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); + + ccr.info = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/info', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ccr.stats = ca({ + urls: [ + { + fmt: '/_ccr/stats', + }, + ], + method: 'GET', + }); + + ccr.followerIndexStats = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/stats', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ccr.saveFollowerIndex = ca({ + urls: [ + { + fmt: '/<%=name%>/_ccr/follow', + req: { + name: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'PUT', + }); + + ccr.pauseFollowerIndex = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/pause_follow', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); + + ccr.resumeFollowerIndex = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/resume_follow', + req: { + id: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'POST', + }); + + ccr.unfollowLeaderIndex = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/unfollow', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/config.ts b/x-pack/plugins/cross_cluster_replication/server/config.ts new file mode 100644 index 0000000000000..17999d37c76b7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/config.ts @@ -0,0 +1,16 @@ +/* + * 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, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), +}); + +export type CrossClusterReplicationConfig = TypeOf; diff --git a/x-pack/plugins/cross_cluster_replication/server/index.ts b/x-pack/plugins/cross_cluster_replication/server/index.ts new file mode 100644 index 0000000000000..597c039ad202e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { CrossClusterReplicationServerPlugin } from './plugin'; +import { configSchema, CrossClusterReplicationConfig } from './config'; + +export const plugin = (pluginInitializerContext: PluginInitializerContext) => + new CrossClusterReplicationServerPlugin(pluginInitializerContext); + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + ui: true, + }, +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap new file mode 100644 index 0000000000000..3eced37112a35 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[CCR] auto-follow stats serialization should deserialize auto-follow stats 1`] = ` +Object { + "autoFollowedClusters": Array [ + Object { + "clusterName": "new-york", + "lastSeenMetadataVersion": 15, + "timeSinceLastCheckMillis": 2426, + }, + ], + "numberOfFailedFollowIndices": 0, + "numberOfFailedRemoteClusterStateRequests": 0, + "numberOfSuccessfulFollowIndices": 0, + "recentAutoFollowErrors": Array [ + Object { + "autoFollowException": Object { + "reason": "index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]", + "type": "exception", + }, + "leaderIndex": "pattern-1:kibana_sample_1", + "timestamp": 1587081600021, + }, + Object { + "autoFollowException": Object { + "reason": "index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]", + "type": "exception", + }, + "leaderIndex": "pattern-2:kibana_sample_1", + "timestamp": 1587081600021, + }, + ], +} +`; diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts new file mode 100644 index 0000000000000..5141aa56c1d7e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { deserializeAutoFollowStats } from './ccr_stats_serialization'; + +describe('[CCR] auto-follow stats serialization', () => { + it('should deserialize auto-follow stats', () => { + const esObject = { + number_of_failed_follow_indices: 0, + number_of_failed_remote_cluster_state_requests: 0, + number_of_successful_follow_indices: 0, + recent_auto_follow_errors: [ + { + leader_index: 'pattern-1:kibana_sample_1', + timestamp: 1587081600021, + auto_follow_exception: { + type: 'exception', + reason: + 'index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]', + }, + }, + { + leader_index: 'pattern-2:kibana_sample_1', + timestamp: 1587081600021, + auto_follow_exception: { + type: 'exception', + reason: + 'index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]', + }, + }, + ], + auto_followed_clusters: [ + { + cluster_name: 'new-york', + time_since_last_check_millis: 2426, + last_seen_metadata_version: 15, + }, + ], + }; + + expect(deserializeAutoFollowStats(esObject)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts new file mode 100644 index 0000000000000..7e2b088919842 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts @@ -0,0 +1,51 @@ +/* + * 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 { + RecentAutoFollowError, + RecentAutoFollowErrorFromEs, + AutoFollowedCluster, + AutoFollowedClusterFromEs, + AutoFollowStats, + AutoFollowStatsFromEs, +} from '../../common/types'; + +export const deserializeRecentAutoFollowErrors = ({ + timestamp, + leader_index, + auto_follow_exception: { type, reason }, +}: RecentAutoFollowErrorFromEs): RecentAutoFollowError => ({ + timestamp, + leaderIndex: leader_index, + autoFollowException: { + type, + reason, + }, +}); + +export const deserializeAutoFollowedClusters = ({ + cluster_name, + time_since_last_check_millis, + last_seen_metadata_version, +}: AutoFollowedClusterFromEs): AutoFollowedCluster => ({ + clusterName: cluster_name, + timeSinceLastCheckMillis: time_since_last_check_millis, + lastSeenMetadataVersion: last_seen_metadata_version, +}); + +export const deserializeAutoFollowStats = ({ + number_of_failed_follow_indices, + number_of_failed_remote_cluster_state_requests, + number_of_successful_follow_indices, + recent_auto_follow_errors, + auto_followed_clusters, +}: AutoFollowStatsFromEs): AutoFollowStats => ({ + numberOfFailedFollowIndices: number_of_failed_follow_indices, + numberOfFailedRemoteClusterStateRequests: number_of_failed_remote_cluster_state_requests, + numberOfSuccessfulFollowIndices: number_of_successful_follow_indices, + recentAutoFollowErrors: recent_auto_follow_errors.map(deserializeRecentAutoFollowErrors), + autoFollowedClusters: auto_followed_clusters.map(deserializeAutoFollowedClusters), +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts new file mode 100644 index 0000000000000..9dde027cd6949 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts @@ -0,0 +1,78 @@ +/* + * 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. + */ + +function extractCausedByChain( + causedBy: Record = {}, + accumulator: string[] = [] +): string[] { + const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase + + if (reason) { + accumulator.push(reason); + } + + // eslint-disable-next-line @typescript-eslint/camelcase + if (caused_by) { + return extractCausedByChain(caused_by, accumulator); + } + + return accumulator; +} + +/** + * Wraps an error thrown by the ES JS client into a Boom error response and returns it + * + * @param err Object Error thrown by ES JS client + * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages + */ +export function wrapEsError( + err: any, + statusCodeToMessageMap: Record = {} +): { message: string; body?: { cause?: string[] }; statusCode: number } { + const { statusCode, response } = err; + + const { + error: { + root_cause = [], // eslint-disable-line @typescript-eslint/camelcase + caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase + } = {}, + } = JSON.parse(response); + + // If no custom message if specified for the error's status code, just + // wrap the error as a Boom error response and return it + if (!statusCodeToMessageMap[statusCode]) { + // The caused_by chain has the most information so use that if it's available. If not then + // settle for the root_cause. + const causedByChain = extractCausedByChain(caused_by); + const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; + + return { + message: err.message, + statusCode, + body: { + cause: causedByChain.length ? causedByChain : defaultCause, + }, + }; + } + + // Otherwise, use the custom message to create a Boom error response and + // return it + const message = statusCodeToMessageMap[statusCode]; + return { message, statusCode }; +} + +export function formatEsError(err: any): any { + const { statusCode, message, body } = wrapEsError(err); + return { + statusCode, + body: { + message, + attributes: { + cause: body?.cause, + }, + }, + }; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts rename to x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts new file mode 100644 index 0000000000000..25c99803480f3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/plugin.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. + */ + +declare module 'src/core/server' { + interface RequestHandlerContext { + crossClusterReplication?: CrossClusterReplicationContext; + } +} + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import { + CoreSetup, + Plugin, + Logger, + PluginInitializerContext, + APICaller, + IScopedClusterClient, +} from 'src/core/server'; + +import { Index } from '../../index_management/server'; +import { PLUGIN } from '../common/constants'; +import { Dependencies } from './types'; +import { registerApiRoutes } from './routes'; +import { License } from './services'; +import { elasticsearchJsPlugin } from './client/elasticsearch_ccr'; +import { CrossClusterReplicationConfig } from './config'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; + +interface CrossClusterReplicationContext { + client: IScopedClusterClient; +} + +const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => { + if (!indicesList?.length) { + return indicesList; + } + const params = { + path: '/_all/_ccr/info', + method: 'GET', + }; + try { + const { follower_indices: followerIndices } = await callWithRequest( + 'transport.request', + params + ); + return indicesList.map(index => { + const isFollowerIndex = !!followerIndices.find( + (followerIndex: { follower_index: string }) => { + return followerIndex.follower_index === index.name; + } + ); + return { + ...index, + isFollowerIndex, + }; + }); + } catch (e) { + return indicesList; + } +}; + +export class CrossClusterReplicationServerPlugin implements Plugin { + private readonly config$: Observable; + private readonly license: License; + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.config$ = initializerContext.config.create(); + this.license = new License(); + } + + setup( + { http, elasticsearch }: CoreSetup, + { licensing, indexManagement, remoteClusters }: Dependencies + ) { + this.config$ + .pipe(first()) + .toPromise() + .then(config => { + // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting. + // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if + // the Remote Clusters UI is disabled we can't show the CCR UI. + const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; + + // If the UI isn't enabled, then we don't want to expose any CCR concepts in the UI, including + // "follower" badges for follower indices. + if (isCcrUiEnabled) { + if (indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(ccrDataEnricher); + } + } + }); + + this.license.setup( + { + pluginId: PLUGIN.ID, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate( + 'xpack.crossClusterReplication.licenseCheckErrorMessage', + { + defaultMessage: 'License check failed', + } + ), + }, + { + licensing, + logger: this.logger, + } + ); + + // Extend the elasticsearchJs client with additional endpoints. + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + const ccrEsClient = elasticsearch.createClient('crossClusterReplication', esClientConfig); + http.registerRouteHandlerContext('crossClusterReplication', (ctx, request) => { + return { + client: ccrEsClient.asScoped(request), + }; + }); + + registerApiRoutes({ + router: http.createRouter(), + license: this.license, + lib: { + isEsError, + formatEsError, + }, + }); + } + + start() {} + stop() {} +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts new file mode 100644 index 0000000000000..4cbdc7703a694 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerDeleteRoute } from './register_delete_route'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerGetRoute } from './register_get_route'; +import { registerPauseRoute } from './register_pause_route'; +import { registerResumeRoute } from './register_resume_route'; +import { registerUpdateRoute } from './register_update_route'; + +export function registerAutoFollowPatternRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerDeleteRoute(dependencies); + registerFetchRoute(dependencies); + registerGetRoute(dependencies); + registerPauseRoute(dependencies); + registerResumeRoute(dependencies); + registerUpdateRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts new file mode 100644 index 0000000000000..b41b52e1764c8 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerCreateRoute } from './register_create_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Create auto-follow pattern', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerCreateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('should throw a 409 conflict error if id already exists', async () => { + const routeContextMock = mockRouteContext({ + // Fail the uniqueness check. + callAsCurrentUser: jest.fn().mockResolvedValueOnce(true), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + id: 'some-id', + foo: 'bar', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); + + it('should return 200 status when the id does not exist', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // Pass the uniqueness check. + .mockRejectedValueOnce({ statusCode: 404 }) + .mockResolvedValueOnce(true), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + id: 'some-id', + foo: 'bar', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts new file mode 100644 index 0000000000000..12503e3532a47 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts @@ -0,0 +1,77 @@ +/* + * 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 { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { AutoFollowPattern } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Create an auto-follow pattern + */ +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const bodySchema = schema.object({ + id: schema.string(), + remoteCluster: schema.string(), + leaderIndexPatterns: schema.arrayOf(schema.string()), + followIndexPattern: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id, ...rest } = request.body; + const body = serializeAutoFollowPattern(rest as AutoFollowPattern); + + /** + * First let's make sure that an auto-follow pattern with + * the same id does not exist. + */ + try { + await context.crossClusterReplication!.client.callAsCurrentUser('ccr.autoFollowPattern', { + id, + }); + // If we get here it means that an auto-follow pattern with the same id exists + return response.conflict({ + body: `An auto-follow pattern with the name "${id}" already exists.`, + }); + } catch (err) { + if (err.statusCode !== 404) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + } + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveAutoFollowPattern', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts new file mode 100644 index 0000000000000..e610d09b44275 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerDeleteRoute } from './register_delete_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Delete auto-follow pattern(s)', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerDeleteRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it('deletes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsDeleted).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('deletes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsDeleted).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload.itemsDeleted).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts new file mode 100644 index 0000000000000..ed2633a4a469e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Delete an auto-follow pattern + */ +export const registerDeleteRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.delete( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsDeleted: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(_id => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.deleteAutoFollowPattern', { + id: _id, + }) + .then(() => itemsDeleted.push(_id)) + .catch((err: any) => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsDeleted, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts new file mode 100644 index 0000000000000..dd102c45665cb --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerFetchRoute } from './register_fetch_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Fetch all auto-follow patterns', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerFetchRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('deserializes the response from Elasticsearch', async () => { + const ccrAutoFollowPatternResponseMock = { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.patterns).toEqual([ + { + active: true, + followIndexPattern: 'follow', + leaderIndexPatterns: ['leader*'], + name: 'autoFollowPattern', + remoteCluster: 'remoteCluster', + }, + ]); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts new file mode 100644 index 0000000000000..70d8ae4d51e3b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { deserializeListAutoFollowPatterns } from '../../../../common/services/auto_follow_pattern_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Get a list of all auto-follow patterns + */ +export const registerFetchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/auto_follow_patterns'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const result = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.autoFollowPatterns' + ); + return response.ok({ + body: { + patterns: deserializeListAutoFollowPatterns(result.patterns), + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts new file mode 100644 index 0000000000000..d5889074651f5 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerGetRoute } from './register_get_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Get one auto-follow pattern', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerGetRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return a single resource even though ES returns an array with 1 item', async () => { + const ccrAutoFollowPatternResponseMock = { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload).toEqual({ + active: true, + followIndexPattern: 'follow', + leaderIndexPatterns: ['leader*'], + name: 'autoFollowPattern', + remoteCluster: 'remoteCluster', + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts new file mode 100644 index 0000000000000..1edbf7e8806c7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts @@ -0,0 +1,54 @@ +/* + * 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 { deserializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Get a single auto-follow pattern + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.get( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + try { + const result = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.autoFollowPattern', + { id } + ); + const autoFollowPattern = result.patterns[0]; + + return response.ok({ + body: deserializeAutoFollowPattern(autoFollowPattern), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts new file mode 100644 index 0000000000000..1eaac02918b88 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerPauseRoute } from './register_pause_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Pause auto-follow pattern(s)', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerPauseRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('pauses a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('pauses multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts new file mode 100644 index 0000000000000..325939709e751 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Pause auto-follow pattern(s) + */ +export const registerPauseRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns/{id}/pause'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(_id => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseAutoFollowPattern', { + id: _id, + }) + .then(() => itemsPaused.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts new file mode 100644 index 0000000000000..9839761e701fc --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerResumeRoute } from './register_resume_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Resume auto-follow pattern(s)', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerResumeRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('resumes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('resumes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts new file mode 100644 index 0000000000000..f5e917773704c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Resume auto-follow pattern(s) + */ +export const registerResumeRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns/{id}/resume'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeAutoFollowPattern', { + id: _id, + }) + .then(() => itemsResumed.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts new file mode 100644 index 0000000000000..85f2270ec3aee --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerUpdateRoute } from './register_update_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Update auto-follow pattern', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerUpdateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('should serialize the payload before sending it to Elasticsearch', async () => { + const routeContextMock = mockRouteContext({ + // Just echo back what we send so we can inspect it. + callAsCurrentUser: jest.fn().mockImplementation((endpoint, payload) => payload), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'foo' }, + body: { + remoteCluster: 'bar1', + leaderIndexPatterns: ['bar2'], + followIndexPattern: 'bar3', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + id: 'foo', + body: { + remote_cluster: 'bar1', + leader_index_patterns: ['bar2'], + follow_index_pattern: 'bar3', + }, + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts new file mode 100644 index 0000000000000..836e5f55c5a48 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { AutoFollowPattern } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Update an auto-follow pattern + */ +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + const bodySchema = schema.object({ + active: schema.boolean(), + remoteCluster: schema.string(), + leaderIndexPatterns: schema.arrayOf(schema.string()), + followIndexPattern: schema.string(), + }); + + router.put( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const body = serializeAutoFollowPattern(request.body as AutoFollowPattern); + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveAutoFollowPattern', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts new file mode 100644 index 0000000000000..45c5729535e58 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { RouteDependencies } from '../../../types'; +import { registerPermissionsRoute } from './register_permissions_route'; +import { registerStatsRoute } from './register_stats_route'; + +export function registerCrossClusterReplicationRoutes(dependencies: RouteDependencies) { + registerPermissionsRoute(dependencies); + registerStatsRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts new file mode 100644 index 0000000000000..b8eb5ae14750e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts @@ -0,0 +1,70 @@ +/* + * 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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns whether the user has CCR permissions + */ +export const registerPermissionsRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/permissions'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + if (!license.isEsSecurityEnabled) { + // If security has been disabled in elasticsearch.yml. we'll just let the user use CCR + // because permissions are irrelevant. + return response.ok({ + body: { + hasPermission: true, + missingClusterPrivileges: [], + }, + }); + } + + try { + const { + has_all_requested: hasPermission, + cluster, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.permissions', { + body: { + cluster: ['manage', 'manage_ccr'], + }, + }); + + const missingClusterPrivileges = Object.keys(cluster).reduce( + (permissions: any, permissionName: any) => { + if (!cluster[permissionName]) { + permissions.push(permissionName); + return permissions; + } + }, + [] as any[] + ); + + return response.ok({ + body: { + hasPermission, + missingClusterPrivileges, + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts new file mode 100644 index 0000000000000..d4288cf7303e2 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addBasePath } from '../../../services'; +import { deserializeAutoFollowStats } from '../../../lib/ccr_stats_serialization'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns Auto-follow stats + */ +export const registerStatsRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/stats/auto_follow'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { + auto_follow_stats: autoFollowStats, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + + return response.ok({ + body: deserializeAutoFollowStats(autoFollowStats), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts new file mode 100644 index 0000000000000..f5d8c7a4f5bda --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerGetRoute } from './register_get_route'; +import { registerPauseRoute } from './register_pause_route'; +import { registerResumeRoute } from './register_resume_route'; +import { registerUnfollowRoute } from './register_unfollow_route'; +import { registerUpdateRoute } from './register_update_route'; + +export function registerFollowerIndexRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerFetchRoute(dependencies); + registerGetRoute(dependencies); + registerPauseRoute(dependencies); + registerResumeRoute(dependencies); + registerUnfollowRoute(dependencies); + registerUpdateRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts new file mode 100644 index 0000000000000..bba82b04ce9a0 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerCreateRoute } from './register_create_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Create follower index', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerCreateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('should return 200 status when follower index is created', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + name: 'follower_index', + remoteCluster: 'remote_cluster', + leaderIndex: 'leader_index', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts new file mode 100644 index 0000000000000..acaeedacfdb2a --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts @@ -0,0 +1,65 @@ +/* + * 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 { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; +import { FollowerIndex } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { removeEmptyFields } from '../../../../common/services/utils'; +import { RouteDependencies } from '../../../types'; + +/** + * Create a follower index + */ +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const bodySchema = schema.object({ + name: schema.string(), + remoteCluster: schema.string(), + leaderIndex: schema.string(), + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }); + + router.post( + { + path: addBasePath('/follower_indices'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { name, ...rest } = request.body; + const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex)); + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveFollowerIndex', + { name, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts new file mode 100644 index 0000000000000..151ab84fabf4c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerFetchRoute } from './register_fetch_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Fetch all follower indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerFetchRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('deserializes the response from Elasticsearch', async () => { + const ccrInfoMockResponse = { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + }, + ], + }; + + // These stats correlate to the above follower indices. + const ccrStatsMockResponse = { + follow_stats: { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }, + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce(ccrInfoMockResponse) + .mockResolvedValueOnce(ccrStatsMockResponse), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload.indices).toEqual([ + { + name: 'followerIndexName', + remoteCluster: 'remoteCluster', + leaderIndex: 'leaderIndex', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [ + { + id: 1, + leaderIndex: 'leaderIndex', + leaderGlobalCheckpoint: 1, + leaderMaxSequenceNum: 1, + followerGlobalCheckpoint: 1, + followerMaxSequenceNum: 1, + lastRequestedSequenceNum: 1, + outstandingReadRequestsCount: 1, + outstandingWriteRequestsCount: 1, + writeBufferOperationsCount: 1, + writeBufferSizeBytes: 1, + followerMappingVersion: 1, + followerSettingsVersion: 1, + totalReadTimeMs: 1, + totalReadRemoteExecTimeMs: 1, + successfulReadRequestCount: 1, + failedReadRequestsCount: 1, + operationsReadCount: 1, + bytesReadCount: 1, + totalWriteTimeMs: 1, + successfulWriteRequestsCount: 1, + failedWriteRequestsCount: 1, + operationsWrittenCount: 1, + readExceptions: 1, + timeSinceLastReadMs: 1, + }, + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts new file mode 100644 index 0000000000000..a78901ce174e4 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts @@ -0,0 +1,62 @@ +/* + * 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 { deserializeListFollowerIndices } from '../../../../common/services/follower_index_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a list of all follower indices + */ +export const registerFetchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/follower_indices'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { + id: '_all', + }); + + const { + follow_stats: { indices: followerIndicesStats }, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + + const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { + map[stats.index] = stats; + return map; + }, {}); + + const collatedFollowerIndices = followerIndices.map((followerIndex: any) => { + return { + ...followerIndex, + ...followerIndicesStatsMap[followerIndex.follower_index], + }; + }); + + return response.ok({ + body: { + indices: deserializeListFollowerIndices(collatedFollowerIndices), + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts new file mode 100644 index 0000000000000..42d04ca65b1cb --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerGetRoute } from './register_get_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Get one follower index', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerGetRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return a single resource even though ES returns an array with 1 item', async () => { + const ccrInfoMockResponse = { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + }, + ], + }; + + // These stats correlate to the above follower indices. + const ccrFollowerIndexStatsMockResponse = { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce(ccrInfoMockResponse) + .mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'doesnt_matter' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + name: 'followerIndexName', + remoteCluster: 'remoteCluster', + leaderIndex: 'leaderIndex', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [ + { + id: 1, + leaderIndex: 'leaderIndex', + leaderGlobalCheckpoint: 1, + leaderMaxSequenceNum: 1, + followerGlobalCheckpoint: 1, + followerMaxSequenceNum: 1, + lastRequestedSequenceNum: 1, + outstandingReadRequestsCount: 1, + outstandingWriteRequestsCount: 1, + writeBufferOperationsCount: 1, + writeBufferSizeBytes: 1, + followerMappingVersion: 1, + followerSettingsVersion: 1, + totalReadTimeMs: 1, + totalReadRemoteExecTimeMs: 1, + successfulReadRequestCount: 1, + failedReadRequestsCount: 1, + operationsReadCount: 1, + bytesReadCount: 1, + totalWriteTimeMs: 1, + successfulWriteRequestsCount: 1, + failedWriteRequestsCount: 1, + operationsWrittenCount: 1, + readExceptions: 1, + timeSinceLastReadMs: 1, + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts new file mode 100644 index 0000000000000..98a182fc15681 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts @@ -0,0 +1,78 @@ +/* + * 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 { deserializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a single follower index pattern + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.get( + { + path: addBasePath('/follower_indices/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; + + if (!followerIndexInfo) { + return response.notFound({ + body: `The follower index "${id}" does not exist.`, + }); + } + + // If this follower is paused, skip call to ES stats api since it will return 404 + if (followerIndexInfo.status === 'paused') { + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + }), + }); + } else { + const { + indices: followerIndicesStats, + } = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.followerIndexStats', + { id } + ); + + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + ...(followerIndicesStats ? followerIndicesStats[0] : {}), + }), + }); + } + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts new file mode 100644 index 0000000000000..82cb88cbacea7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerPauseRoute } from './register_pause_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Pause follower index/indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerPauseRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('pauses a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('pauses multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts new file mode 100644 index 0000000000000..7432ea7ca5c82 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts @@ -0,0 +1,64 @@ +/* + * 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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Pauses a follower index + */ +export const registerPauseRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/pause'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseFollowerIndex', { + id: _id, + }) + .then(() => itemsPaused.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts new file mode 100644 index 0000000000000..04167c5db3162 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerResumeRoute } from './register_resume_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Resume follower index/indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerResumeRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('resumes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('resumes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts new file mode 100644 index 0000000000000..ca8f3a9f5fe9d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts @@ -0,0 +1,64 @@ +/* + * 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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Resumes a follower index + */ +export const registerResumeRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/resume'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeFollowerIndex', { + id: _id, + }) + .then(() => itemsResumed.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts new file mode 100644 index 0000000000000..6302d5868b0db --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerUnfollowRoute } from './register_unfollow_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Unfollow follower index/indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerUnfollowRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('unfollows a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('unfollows multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // a + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // b + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // c + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // a + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // b + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts new file mode 100644 index 0000000000000..282fead02bbe0 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts @@ -0,0 +1,95 @@ +/* + * 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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Unfollow follower index's leader index + */ +export const registerUnfollowRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/unfollow'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsUnfollowed: string[] = []; + const itemsNotOpen: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(async (_id: string) => { + try { + // Try to pause follower, let it fail silently since it may already be paused + try { + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.pauseFollowerIndex', + { id: _id } + ); + } catch (e) { + // Swallow errors + } + + // Close index + await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', { + index: _id, + }); + + // Unfollow leader + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.unfollowLeaderIndex', + { id: _id } + ); + + // Try to re-open the index, store failures in a separate array to surface warnings in the UI + // This will allow users to query their index normally after unfollowing + try { + await context.crossClusterReplication!.client.callAsCurrentUser('indices.open', { + index: _id, + }); + } catch (e) { + itemsNotOpen.push(_id); + } + + // Push success + itemsUnfollowed.push(_id); + } catch (err) { + errors.push({ id: _id, error: formatError(err) }); + } + }) + ); + + return response.ok({ + body: { + itemsUnfollowed, + itemsNotOpen, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts new file mode 100644 index 0000000000000..521de77180974 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts @@ -0,0 +1,93 @@ +/* + * 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 { serializeAdvancedSettings } from '../../../../common/services/follower_index_serialization'; +import { FollowerIndexAdvancedSettings } from '../../../../common/types'; +import { removeEmptyFields } from '../../../../common/services/utils'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Update a follower index + */ +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + const bodySchema = schema.object({ + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }); + + router.put( + { + path: addBasePath('/follower_indices/{id}'), + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + // We need to first pause the follower and then resume it by passing the advanced settings + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; + + if (!followerIndexInfo) { + return response.notFound({ body: `The follower index "${id}" does not exist.` }); + } + + // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. + const isPaused = followerIndexInfo.status === 'paused'; + + // Pause follower if not already paused + if (!isPaused) { + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.pauseFollowerIndex', + { + id, + } + ); + } + + // Resume follower + const body = removeEmptyFields( + serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings) + ); + + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.resumeFollowerIndex', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts new file mode 100644 index 0000000000000..9b4fb134ed230 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts @@ -0,0 +1,23 @@ +/* + * 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 { RequestHandlerContext } from 'src/core/server'; + +export function mockRouteContext({ + callAsCurrentUser, +}: { + callAsCurrentUser: any; +}): RequestHandlerContext { + const routeContextMock = ({ + crossClusterReplication: { + client: { + callAsCurrentUser, + }, + }, + } as unknown) as RequestHandlerContext; + + return routeContextMock; +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts new file mode 100644 index 0000000000000..84abfb369e002 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { RouteDependencies } from '../types'; + +import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern'; +import { registerFollowerIndexRoutes } from './api/follower_index'; +import { registerCrossClusterReplicationRoutes } from './api/cross_cluster_replication'; + +export function registerApiRoutes(dependencies: RouteDependencies) { + registerAutoFollowPatternRoutes(dependencies); + registerFollowerIndexRoutes(dependencies); + registerCrossClusterReplicationRoutes(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts new file mode 100644 index 0000000000000..3f3dd131df7c7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { API_BASE_PATH } from '../../common/constants'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/plugins/cross_cluster_replication/server/services/index.ts b/x-pack/plugins/cross_cluster_replication/server/services/index.ts new file mode 100644 index 0000000000000..d7b544b290c39 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/services/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { License } from './license'; +export { addBasePath } from './add_base_path'; diff --git a/x-pack/plugins/cross_cluster_replication/server/services/license.ts b/x-pack/plugins/cross_cluster_replication/server/services/license.ts new file mode 100644 index 0000000000000..bfd357867c3e2 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/services/license.ts @@ -0,0 +1,93 @@ +/* + * 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 } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + private _isEsSecurityEnabled: boolean = false; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + // Retrieving security checks the results of GET /_xpack as well as license state, + // so we're also checking whether the security is disabled in elasticsearch.yml. + this._isEsSecurityEnabled = license.getFeature('security').isEnabled; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + get isEsSecurityEnabled() { + return this._isEsSecurityEnabled; + } +} diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts new file mode 100644 index 0000000000000..049d440e3d85d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/types.ts @@ -0,0 +1,28 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { IndexManagementPluginSetup } from '../../index_management/server'; +import { RemoteClustersPluginSetup } from '../../remote_clusters/server'; +import { License } from './services'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; + +export interface Dependencies { + licensing: LicensingPluginSetup; + indexManagement: IndexManagementPluginSetup; + remoteClusters: RemoteClustersPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + formatEsError: typeof formatEsError; + }; +} diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts index 95f2c9e477064..a7d6aa894d91d 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts @@ -45,9 +45,11 @@ describe('Async search strategy', () => { it('stops polling when the response is complete', async () => { mockSearch - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })) + .mockReturnValueOnce( + of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }) + ); const asyncSearch = asyncSearchStrategyProvider({ core: mockCoreStart, @@ -67,10 +69,39 @@ describe('Async search strategy', () => { expect(mockSearch).toBeCalledTimes(2); }); + it('stops polling when the response is an error', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch + .search(mockRequest, mockOptions) + .toPromise() + .catch(() => { + expect(mockSearch).toBeCalledTimes(2); + }); + }); + it('only sends the ID and server strategy after the first request', async () => { mockSearch - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce( + of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }) + ); const asyncSearch = asyncSearchStrategyProvider({ core: mockCoreStart, diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts index 6271d7fcbeaac..18b5b976b3c1b 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts @@ -14,7 +14,7 @@ import { SYNC_SEARCH_STRATEGY, TSearchStrategyProvider, } from '../../../../../src/plugins/data/public'; -import { IAsyncSearchRequest, IAsyncSearchOptions } from './types'; +import { IAsyncSearchRequest, IAsyncSearchOptions, IAsyncSearchResponse } from './types'; export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY'; @@ -52,9 +52,14 @@ export const asyncSearchStrategyProvider: TSearchStrategyProvider { + expand((response: IAsyncSearchResponse) => { + // If the response indicates of an error, stop polling and complete the observable + if (!response || (response.is_partial && !response.is_running)) { + return throwError(new AbortError()); + } + // If the response indicates it is complete, stop polling and complete the observable - if ((response.loaded ?? 0) >= (response.total ?? 0)) return EMPTY; + if (!response.is_running) return EMPTY; id = response.id; diff --git a/x-pack/plugins/data_enhanced/public/search/types.ts b/x-pack/plugins/data_enhanced/public/search/types.ts index edaaf1b22654d..8ffc8eddda052 100644 --- a/x-pack/plugins/data_enhanced/public/search/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/types.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ISearchOptions, ISyncSearchRequest } from '../../../../../src/plugins/data/public'; +import { + IKibanaSearchResponse, + ISearchOptions, + ISyncSearchRequest, +} from '../../../../../src/plugins/data/public'; export interface IAsyncSearchRequest extends ISyncSearchRequest { /** @@ -19,3 +23,14 @@ export interface IAsyncSearchOptions extends ISearchOptions { */ pollInterval?: number; } + +export interface IAsyncSearchResponse extends IKibanaSearchResponse { + /** + * Indicates whether async search is still in flight + */ + is_running?: boolean; + /** + * Indicates whether the results returned are complete or partial + */ + is_partial?: boolean; +} diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 6b329bccab4a7..bf502889ffa4f 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -23,6 +23,8 @@ import { shimHitsTotal } from './shim_hits_total'; export interface AsyncSearchResponse { id: string; + is_partial: boolean; + is_running: boolean; response: SearchResponse; } @@ -71,13 +73,19 @@ async function asyncSearch( // Wait up to 1s for the response to return const query = toSnakeCase({ waitForCompletionTimeout: '1s', ...queryParams }); - const { response, id } = (await caller( + const { id, response, is_partial, is_running } = (await caller( 'transport.request', { method, path, body, query }, options )) as AsyncSearchResponse; - return { id, rawResponse: shimHitsTotal(response), ...getTotalLoaded(response._shards) }; + return { + id, + is_partial, + is_running, + rawResponse: shimHitsTotal(response), + ...getTotalLoaded(response._shards), + }; } async function rollupSearch( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts index 21871ec8ca849..56a49df3bdab4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostListPagination, ServerApiError } from '../../types'; +import { ServerApiError } from '../../types'; import { HostResultList, HostInfo } from '../../../../../common/types'; interface ServerReturnedHostList { @@ -12,6 +12,11 @@ interface ServerReturnedHostList { payload: HostResultList; } +interface ServerFailedToReturnHostList { + type: 'serverFailedToReturnHostList'; + payload: ServerApiError; +} + interface ServerReturnedHostDetails { type: 'serverReturnedHostDetails'; payload: HostInfo; @@ -22,13 +27,8 @@ interface ServerFailedToReturnHostDetails { payload: ServerApiError; } -interface UserPaginatedHostList { - type: 'userPaginatedHostList'; - payload: HostListPagination; -} - export type HostAction = | ServerReturnedHostList + | ServerFailedToReturnHostList | ServerReturnedHostDetails - | ServerFailedToReturnHostDetails - | UserPaginatedHostList; + | ServerFailedToReturnHostDetails; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts new file mode 100644 index 0000000000000..d2e1985d055c6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { CoreStart, HttpSetup } from 'kibana/public'; +import { DepsStartMock, depsStartMock } from '../../mocks'; +import { AppAction, HostState, HostIndexUIQueryParams } from '../../types'; +import { Immutable, HostResultList } from '../../../../../common/types'; +import { History, createBrowserHistory } from 'history'; +import { hostMiddlewareFactory } from './middleware'; +import { applyMiddleware, Store, createStore } from 'redux'; +import { hostListReducer } from './reducer'; +import { coreMock } from 'src/core/public/mocks'; +import { urlFromQueryParams } from '../../view/hosts/url_from_query_params'; +import { uiQueryParams } from './selectors'; +import { mockHostResultList } from './mock_host_result_list'; +import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../test_utils'; + +describe('host list pagination: ', () => { + let fakeCoreStart: jest.Mocked; + let depsStart: DepsStartMock; + let fakeHttpServices: jest.Mocked; + let history: History; + let store: Store, Immutable>; + let queryParams: () => HostIndexUIQueryParams; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let actionSpyMiddleware; + const getEndpointListApiResponse = (): HostResultList => { + return mockHostResultList({ request_page_size: 1, request_page_index: 1, total: 10 }); + }; + + let historyPush: (params: HostIndexUIQueryParams) => void; + beforeEach(() => { + fakeCoreStart = coreMock.createStart(); + depsStart = depsStartMock(); + fakeHttpServices = fakeCoreStart.http as jest.Mocked; + history = createBrowserHistory(); + const middleware = hostMiddlewareFactory(fakeCoreStart, depsStart); + ({ actionSpyMiddleware, waitForAction } = createSpyMiddleware()); + store = createStore(hostListReducer, applyMiddleware(middleware, actionSpyMiddleware)); + + history.listen(location => { + store.dispatch({ type: 'userChangedUrl', payload: location }); + }); + + queryParams = () => uiQueryParams(store.getState()); + + historyPush = (nextQueryParams: HostIndexUIQueryParams): void => { + return history.push(urlFromQueryParams(nextQueryParams)); + }; + }); + + describe('when the user enteres the host list for the first time', () => { + it('the api is called with page_index and page_size defaulting to 0 and 10 respectively', async () => { + const apiResponse = getEndpointListApiResponse(); + fakeHttpServices.post.mockResolvedValue(apiResponse); + expect(fakeHttpServices.post).not.toHaveBeenCalled(); + + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + }, + }); + await waitForAction('serverReturnedHostList'); + expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: '0' }, { page_size: '10' }], + }), + }); + }); + }); + describe('when a new page size is passed', () => { + it('should modify the url correctly', () => { + historyPush({ ...queryParams(), page_size: '20' }); + expect(queryParams()).toMatchInlineSnapshot(` + Object { + "page_index": "0", + "page_size": "20", + } + `); + }); + }); + describe('when an invalid page size is passed', () => { + it('should modify the page size in the url to the default page size', () => { + historyPush({ ...queryParams(), page_size: '1' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when a negative page size is passed', () => { + it('should modify the page size in the url to the default page size', () => { + historyPush({ ...queryParams(), page_size: '-1' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when a new page index is passed', () => { + it('should modify the page index in the url correctly', () => { + historyPush({ ...queryParams(), page_index: '2' }); + expect(queryParams()).toEqual({ page_index: '2', page_size: '10' }); + }); + }); + + describe('when a negative page index is passed', () => { + it('should modify the page index in the url to the default page index', () => { + historyPush({ ...queryParams(), page_index: '-2' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when invalid params are passed in the url', () => { + it('ignores non-numeric values for page_index and page_size', () => { + historyPush({ ...queryParams, page_index: 'one', page_size: 'fifty' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + + it('ignores unknown url search params', () => { + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + search: '?foo=bar', + }, + }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + + it('ignores multiple values of the same query params except the last value', () => { + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + search: '?page_index=2&page_index=3&page_size=20&page_size=50', + }, + }); + expect(queryParams()).toEqual({ page_index: '3', page_size: '50' }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts index 6148934343635..515c54eab3280 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts @@ -6,12 +6,12 @@ import { createStore, Dispatch, Store } from 'redux'; import { HostAction, hostListReducer } from './index'; -import { HostListState } from '../../types'; +import { HostState } from '../../types'; import { listData } from './selectors'; import { mockHostResultList } from './mock_host_result_list'; describe('HostList store concerns', () => { - let store: Store; + let store: Store; let dispatch: Dispatch; const createTestStore = () => { store = createStore(hostListReducer); @@ -37,6 +37,11 @@ describe('HostList store concerns', () => { pageIndex: 0, total: 0, loading: false, + error: undefined, + details: undefined, + detailsLoading: false, + detailsError: undefined, + location: undefined, }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index 8f39baddda00e..1af83a975d1d8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -9,21 +9,23 @@ import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; import { hostListReducer, hostMiddlewareFactory } from './index'; import { HostResultList, Immutable } from '../../../../../common/types'; -import { HostListState } from '../../types'; +import { HostState } from '../../types'; import { AppAction } from '../action'; import { listData } from './selectors'; import { DepsStartMock, depsStartMock } from '../../mocks'; import { mockHostResultList } from './mock_host_result_list'; +import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils'; describe('host list middleware', () => { - const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); let fakeCoreStart: jest.Mocked; let depsStart: DepsStartMock; let fakeHttpServices: jest.Mocked; - type HostListStore = Store, Immutable>; + type HostListStore = Store, Immutable>; let store: HostListStore; let getState: HostListStore['getState']; let dispatch: HostListStore['dispatch']; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let actionSpyMiddleware; let history: History; const getEndpointListApiResponse = (): HostResultList => { @@ -33,15 +35,16 @@ describe('host list middleware', () => { fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); depsStart = depsStartMock(); fakeHttpServices = fakeCoreStart.http as jest.Mocked; + ({ actionSpyMiddleware, waitForAction } = createSpyMiddleware()); store = createStore( hostListReducer, - applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart)) + applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart), actionSpyMiddleware) ); getState = store.getState; dispatch = store.dispatch; history = createBrowserHistory(); }); - test('handles `userChangedUrl`', async () => { + it('handles `userChangedUrl`', async () => { const apiResponse = getEndpointListApiResponse(); fakeHttpServices.post.mockResolvedValue(apiResponse); expect(fakeHttpServices.post).not.toHaveBeenCalled(); @@ -53,10 +56,10 @@ describe('host list middleware', () => { pathname: '/hosts', }, }); - await sleep(); + await waitForAction('serverReturnedHostList'); expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { body: JSON.stringify({ - paging_properties: [{ page_index: 0 }, { page_size: 10 }], + paging_properties: [{ page_index: '0' }, { page_size: '10' }], }), }); expect(listData(getState())).toEqual(apiResponse.hosts.map(hostInfo => hostInfo.metadata)); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index 83e11f5408bcd..bb1cfc4dd10af 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -4,34 +4,64 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HostResultList } from '../../../../../common/types'; +import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors'; +import { HostState } from '../../types'; import { ImmutableMiddlewareFactory } from '../../types'; -import { pageIndex, pageSize, isOnHostPage, hasSelectedHost, uiQueryParams } from './selectors'; -import { HostListState } from '../../types'; -export const hostMiddlewareFactory: ImmutableMiddlewareFactory = coreStart => { +export const hostMiddlewareFactory: ImmutableMiddlewareFactory = coreStart => { return ({ getState, dispatch }) => next => async action => { next(action); const state = getState(); if ( - (action.type === 'userChangedUrl' && - isOnHostPage(state) && - hasSelectedHost(state) !== true) || - action.type === 'userPaginatedHostList' + action.type === 'userChangedUrl' && + isOnHostPage(state) && + hasSelectedHost(state) !== true ) { - const hostPageIndex = pageIndex(state); - const hostPageSize = pageSize(state); - const response = await coreStart.http.post('/api/endpoint/metadata', { - body: JSON.stringify({ - paging_properties: [{ page_index: hostPageIndex }, { page_size: hostPageSize }], - }), - }); - response.request_page_index = hostPageIndex; - dispatch({ - type: 'serverReturnedHostList', - payload: response, - }); + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state); + try { + const response = await coreStart.http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedHostList', + payload: response, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnHostList', + payload: error, + }); + } } if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) { + // If user navigated directly to a host details page, load the host list + if (listData(state).length === 0) { + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state); + try { + const response = await coreStart.http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedHostList', + payload: response, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnHostList', + payload: error, + }); + return; + } + } + + // call the host details api const { selected_host: selectedHost } = uiQueryParams(state); try { const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index 298e819645dbe..adf18fa50c24f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -4,23 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostListState, ImmutableReducer } from '../../types'; +import { Immutable } from '../../../../../common/types'; +import { HostState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; +import { isOnHostPage, hasSelectedHost } from './selectors'; -const initialState = (): HostListState => { +const initialState = (): HostState => { return { hosts: [], pageSize: 10, pageIndex: 0, total: 0, loading: false, - detailsError: undefined, + error: undefined, details: undefined, + detailsLoading: false, + detailsError: undefined, location: undefined, }; }; -export const hostListReducer: ImmutableReducer = ( +export const hostListReducer: ImmutableReducer = ( state = initialState(), action ) => { @@ -38,30 +42,77 @@ export const hostListReducer: ImmutableReducer = ( pageSize, pageIndex, loading: false, + error: undefined, + }; + } else if (action.type === 'serverFailedToReturnHostList') { + return { + ...state, + error: action.payload, + loading: false, }; } else if (action.type === 'serverReturnedHostDetails') { return { ...state, details: action.payload.metadata, + detailsLoading: false, + detailsError: undefined, }; } else if (action.type === 'serverFailedToReturnHostDetails') { return { ...state, detailsError: action.payload, + detailsLoading: false, }; - } else if (action.type === 'userPaginatedHostList') { - return { + } else if (action.type === 'userChangedUrl') { + const newState: Immutable = { ...state, - ...action.payload, - loading: true, + location: action.payload, }; - } else if (action.type === 'userChangedUrl') { + const isCurrentlyOnListPage = isOnHostPage(newState) && !hasSelectedHost(newState); + const wasPreviouslyOnListPage = isOnHostPage(state) && !hasSelectedHost(state); + const isCurrentlyOnDetailsPage = isOnHostPage(newState) && hasSelectedHost(newState); + const wasPreviouslyOnDetailsPage = isOnHostPage(state) && hasSelectedHost(state); + + // if on the host list page for the first time, return new location and load list + if (isCurrentlyOnListPage) { + if (!wasPreviouslyOnListPage) { + return { + ...state, + location: action.payload, + loading: true, + error: undefined, + detailsError: undefined, + }; + } + } else if (isCurrentlyOnDetailsPage) { + // if previous page was the list or another host details page, load host details only + if (wasPreviouslyOnDetailsPage || wasPreviouslyOnListPage) { + return { + ...state, + location: action.payload, + detailsLoading: true, + error: undefined, + detailsError: undefined, + }; + } else { + // if previous page was not host list or host details, load both list and details + return { + ...state, + location: action.payload, + loading: true, + detailsLoading: true, + error: undefined, + detailsError: undefined, + }; + } + } + // otherwise we are not on a host list or details page return { ...state, location: action.payload, + error: undefined, detailsError: undefined, }; } - return state; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts index 03cdba8505800..b0f949ebbe757 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -6,38 +6,47 @@ import querystring from 'querystring'; import { createSelector } from 'reselect'; import { Immutable } from '../../../../../common/types'; -import { HostListState, HostIndexUIQueryParams } from '../../types'; +import { HostState, HostIndexUIQueryParams } from '../../types'; -export const listData = (state: Immutable) => state.hosts; +const PAGE_SIZES = Object.freeze([10, 20, 50]); -export const pageIndex = (state: Immutable) => state.pageIndex; +export const listData = (state: Immutable) => state.hosts; -export const pageSize = (state: Immutable) => state.pageSize; +export const pageIndex = (state: Immutable): number => state.pageIndex; -export const totalHits = (state: Immutable) => state.total; +export const pageSize = (state: Immutable): number => state.pageSize; -export const isLoading = (state: Immutable) => state.loading; +export const totalHits = (state: Immutable): number => state.total; -export const detailsError = (state: Immutable) => state.detailsError; +export const listLoading = (state: Immutable): boolean => state.loading; -export const detailsData = (state: Immutable) => { - return state.details; -}; +export const listError = (state: Immutable) => state.error; -export const isOnHostPage = (state: Immutable) => +export const detailsData = (state: Immutable) => state.details; + +export const detailsLoading = (state: Immutable): boolean => state.detailsLoading; + +export const detailsError = (state: Immutable) => state.detailsError; + +export const isOnHostPage = (state: Immutable) => state.location ? state.location.pathname === '/hosts' : false; export const uiQueryParams: ( - state: Immutable + state: Immutable ) => Immutable = createSelector( - (state: Immutable) => state.location, - (location: Immutable['location']) => { - const data: HostIndexUIQueryParams = {}; + (state: Immutable) => state.location, + (location: Immutable['location']) => { + const data: HostIndexUIQueryParams = { page_index: '0', page_size: '10' }; if (location) { // Removes the `?` from the beginning of query string if it exists const query = querystring.parse(location.search.slice(1)); - const keys: Array = ['selected_host', 'show']; + const keys: Array = [ + 'selected_host', + 'page_size', + 'page_index', + 'show', + ]; for (const key of keys) { const value = query[key]; @@ -47,12 +56,23 @@ export const uiQueryParams: ( data[key] = value[value.length - 1]; } } + + // Check if page size is an expected size, otherwise default to 10 + if (!PAGE_SIZES.includes(Number(data.page_size))) { + data.page_size = '10'; + } + + // Check if page index is a valid positive integer, otherwise default to 0 + const pageIndexAsNumber = Number(data.page_index); + if (!Number.isFinite(pageIndexAsNumber) || pageIndexAsNumber < 0) { + data.page_index = '0'; + } } return data; } ); -export const hasSelectedHost: (state: Immutable) => boolean = createSelector( +export const hasSelectedHost: (state: Immutable) => boolean = createSelector( uiQueryParams, ({ selected_host: selectedHost }) => { return selectedHost !== undefined; @@ -60,7 +80,7 @@ export const hasSelectedHost: (state: Immutable) => boolean = cre ); /** What policy details panel view to show */ -export const showView: (state: HostListState) => 'policy_response' | 'details' = createSelector( +export const showView: (state: HostState) => 'policy_response' | 'details' = createSelector( uiQueryParams, searchParams => { return searchParams.show === 'policy_response' ? 'policy_response' : 'details'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index f407d32cb3b42..e5e600f6c6288 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -88,23 +88,40 @@ export type SubstateMiddlewareFactory = ( middleware: ImmutableMiddleware ) => Middleware<{}, GlobalState, Dispatch>>; -export interface HostListState { +export interface HostState { + /** list of host **/ hosts: HostMetadata[]; + /** number of items per page */ pageSize: number; + /** which page to show */ pageIndex: number; + /** total number of hosts returned */ total: number; + /** list page is retrieving data */ loading: boolean; - detailsError?: ServerApiError; + /** api error from retrieving host list */ + error?: ServerApiError; + /** details data for a specific host */ details?: Immutable; + /** details page is retrieving data */ + detailsLoading: boolean; + /** api error from retrieving host details */ + detailsError?: ServerApiError; + /** current location info */ location?: Immutable; } -export interface HostListPagination { - pageIndex: number; - pageSize: number; -} +/** + * Query params on the host page parsed from the URL + */ export interface HostIndexUIQueryParams { + /** Selected host id shows host details flyout */ selected_host?: string; + /** How many items to show in list */ + page_size?: string; + /** Which page to show */ + page_index?: string; + /** show the policy response or host details */ show?: string; } @@ -257,7 +274,7 @@ export type KeysByValueCriteria = { export type MalwareProtectionOSes = KeysByValueCriteria; export interface GlobalState { - readonly hostList: HostListState; + readonly hostList: HostState; readonly alertList: AlertListState; readonly policyList: PolicyListState; readonly policyDetails: PolicyDetailsState; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx index 6c294d9c86548..7475229853698 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEvent, useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTabs, EuiTab } from '@elastic/eui'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Immutable } from '../../../../../common/types'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; interface NavTabs { name: string; @@ -48,33 +49,30 @@ const navTabs: Immutable = [ }, ]; -export const HeaderNavigation: React.FunctionComponent = React.memo(() => { - const history = useHistory(); - const location = useLocation(); +const NavTab = memo<{ tab: NavTabs }>(({ tab }) => { + const { pathname } = useLocation(); const { services } = useKibana(); + const onClickHandler = useNavigateByRouterEventHandler(tab.href); const BASE_PATH = services.application.getUrlForApp('endpoint'); + return ( + + {tab.name} + + ); +}); + +export const HeaderNavigation: React.FunctionComponent = React.memo(() => { const tabList = useMemo(() => { return navTabs.map((tab, index) => { - return ( - { - event.preventDefault(); - history.push(tab.href); - }} - isSelected={ - tab.href === location.pathname || - (tab.href !== '/' && location.pathname.startsWith(tab.href)) - } - > - {tab.name} - - ); + return ; }); - }, [BASE_PATH, history, location.pathname]); + }, []); return {tabList}; }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx index d0a8f9690dafb..2d4d1ca8a1b5b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx @@ -110,7 +110,7 @@ describe('LinkToApp component', () => { const clickEventArg = spyOnClickHandler.mock.calls[0][0]; expect(clickEventArg.isDefaultPrevented()).toBe(true); }); - it('should not navigate if onClick callback prevents defalut', () => { + it('should not navigate if onClick callback prevents default', () => { const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(ev => { ev.preventDefault(); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx new file mode 100644 index 0000000000000..b1f09617f0174 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; +import { useNavigateByRouterEventHandler } from './use_navigate_by_router_event_handler'; +import { act, fireEvent, cleanup } from '@testing-library/react'; + +type ClickHandlerMock = jest.Mock< + Return, + [React.MouseEvent] +>; + +describe('useNavigateByRouterEventHandler hook', () => { + let render: AppContextTestRender['render']; + let history: AppContextTestRender['history']; + let renderResult: ReturnType; + let linkEle: HTMLAnchorElement; + let clickHandlerSpy: ClickHandlerMock; + const Link = React.memo<{ + routeTo: Parameters[0]; + onClick?: Parameters[1]; + }>(({ routeTo, onClick }) => { + const onClickHandler = useNavigateByRouterEventHandler(routeTo, onClick); + return ( + + mock link + + ); + }); + + beforeEach(async () => { + ({ render, history } = createAppRootMockRenderer()); + clickHandlerSpy = jest.fn(); + renderResult = render(); + linkEle = (await renderResult.findByText('mock link')) as HTMLAnchorElement; + }); + afterEach(cleanup); + + it('should navigate to path via Router', () => { + const containerClickSpy = jest.fn(); + renderResult.container.addEventListener('click', containerClickSpy); + expect(history.location.pathname).not.toEqual('/mock/path'); + act(() => { + fireEvent.click(linkEle); + }); + expect(containerClickSpy.mock.calls[0][0].defaultPrevented).toBe(true); + expect(history.location.pathname).toEqual('/mock/path'); + renderResult.container.removeEventListener('click', containerClickSpy); + }); + it('should support onClick prop', () => { + act(() => { + fireEvent.click(linkEle); + }); + expect(clickHandlerSpy).toHaveBeenCalled(); + expect(history.location.pathname).toEqual('/mock/path'); + }); + it('should not navigate if preventDefault is true', () => { + clickHandlerSpy.mockImplementation(event => { + event.preventDefault(); + }); + act(() => { + fireEvent.click(linkEle); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not navigate via router if click was not the primary mouse button', async () => { + act(() => { + fireEvent.click(linkEle, { button: 2 }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not navigate via router if anchor has target', () => { + linkEle.setAttribute('target', '_top'); + act(() => { + fireEvent.click(linkEle, { button: 2 }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not to navigate if meta|alt|ctrl|shift keys are pressed', () => { + ['meta', 'alt', 'ctrl', 'shift'].forEach(key => { + act(() => { + fireEvent.click(linkEle, { [`${key}Key`]: true }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts new file mode 100644 index 0000000000000..dc33f0befaf35 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts @@ -0,0 +1,70 @@ +/* + * 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 { MouseEventHandler, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { LocationDescriptorObject } from 'history'; + +type EventHandlerCallback = MouseEventHandler; + +/** + * Provides an event handler that can be used with (for example) `onClick` props to prevent the + * event's default behaviour and instead navigate to to a route via the Router + * + * @param routeTo + * @param onClick + */ +export const useNavigateByRouterEventHandler = ( + routeTo: string | [string, unknown] | LocationDescriptorObject, // Cover the calling signature of `history.push()` + + /** Additional onClick callback */ + onClick?: EventHandlerCallback +): EventHandlerCallback => { + const history = useHistory(); + return useCallback( + ev => { + try { + if (onClick) { + onClick(ev); + } + } catch (error) { + ev.preventDefault(); + throw error; + } + + if (ev.defaultPrevented) { + return; + } + + if (ev.button !== 0) { + return; + } + + if ( + ev.currentTarget instanceof HTMLAnchorElement && + ev.currentTarget.target !== '' && + ev.currentTarget.target !== '_self' + ) { + return; + } + + if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { + return; + } + + ev.preventDefault(); + + if (Array.isArray(routeTo)) { + history.push(...routeTo); + } else if (typeof routeTo === 'string') { + history.push(routeTo); + } else { + history.push(routeTo); + } + }, + [history, onClick, routeTo] + ); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx index 26f2203790a9e..02f91307c988e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, MouseEventHandler } from 'react'; import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui'; import styled from 'styled-components'; @@ -12,7 +12,7 @@ export type FlyoutSubHeaderProps = CommonProps & { children: React.ReactNode; backButton?: { title: string; - onClick: (event: React.MouseEvent) => void; + onClick: MouseEventHandler; href?: string; }; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx index 32c69426b03f3..e4da39d50304a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx @@ -16,13 +16,13 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; import { HostMetadata } from '../../../../../../common/types'; import { FormattedDateAndTime } from '../../formatted_date_time'; import { LinkToApp } from '../../components/link_to_app'; -import { useHostListSelector, useHostLogsUrl } from '../hooks'; +import { useHostSelector, useHostLogsUrl } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; import { uiQueryParams } from '../../../store/hosts/selectors'; +import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -33,8 +33,7 @@ const HostIds = styled(EuiListGroupItem)` export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { appId, appPath, url } = useHostLogsUrl(details.host.id); - const queryParams = useHostListSelector(uiQueryParams); - const history = useHistory(); + const queryParams = useHostSelector(uiQueryParams); const detailsResultsUpper = useMemo(() => { return [ { @@ -65,6 +64,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { show: 'policy_response', }); }, [details.host.id, queryParams]); + const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseUri); const detailsResultsLower = useMemo(() => { return [ @@ -84,10 +84,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { { - ev.preventDefault(); - history.push(policyResponseUri); - }} + onClick={policyStatusClickHandler} > { details.endpoint.policy.id, details.host.hostname, details.host.ip, - history, - policyResponseUri, + policyResponseUri.search, + policyStatusClickHandler, ]); return ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx index a41d4a968f177..e44a45f300daa 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx @@ -17,22 +17,30 @@ import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { useHostListSelector } from '../hooks'; +import { useHostSelector } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; -import { uiQueryParams, detailsData, detailsError, showView } from '../../../store/hosts/selectors'; +import { + uiQueryParams, + detailsData, + detailsError, + showView, + detailsLoading, +} from '../../../store/hosts/selectors'; import { HostDetails } from './host_details'; import { PolicyResponse } from './policy_response'; import { HostMetadata } from '../../../../../../common/types'; import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; +import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; export const HostDetailsFlyout = memo(() => { const history = useHistory(); const { notifications } = useKibana(); - const queryParams = useHostListSelector(uiQueryParams); + const queryParams = useHostSelector(uiQueryParams); const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; - const details = useHostListSelector(detailsData); - const error = useHostListSelector(detailsError); - const show = useHostListSelector(showView); + const details = useHostSelector(detailsData); + const loading = useHostSelector(detailsLoading); + const error = useHostSelector(detailsError); + const show = useHostSelector(showView); const handleFlyoutClose = useCallback(() => { history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); @@ -63,7 +71,7 @@ export const HostDetailsFlyout = memo(() => {

- {details === undefined ? : details.host.hostname} + {loading ? : details?.host?.hostname}

@@ -92,24 +100,25 @@ export const HostDetailsFlyout = memo(() => { const PolicyResponseFlyoutPanel = memo<{ hostMeta: HostMetadata; }>(({ hostMeta }) => { - const history = useHistory(); - const { show, ...queryParams } = useHostListSelector(uiQueryParams); + const { show, ...queryParams } = useHostSelector(uiQueryParams); + const detailsUri = useMemo( + () => + urlFromQueryParams({ + ...queryParams, + selected_host: hostMeta.host.id, + }), + [hostMeta.host.id, queryParams] + ); + const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsUri); const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { - const detailsUri = urlFromQueryParams({ - ...queryParams, - selected_host: hostMeta.host.id, - }); return { title: i18n.translate('xpack.endpoint.host.policyResponse.backLinkTitle', { defaultMessage: 'Endpoint Details', }), href: '?' + detailsUri.search, - onClick: ev => { - ev.preventDefault(); - history.push(detailsUri); - }, + onClick: backToDetailsClickHandler, }; - }, [history, hostMeta.host.id, queryParams]); + }, [backToDetailsClickHandler, detailsUri.search]); return ( <> diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts index 7eb51f3a7b294..eb242f5c535f4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts @@ -6,10 +6,10 @@ import { useSelector } from 'react-redux'; import { useMemo } from 'react'; -import { GlobalState, HostListState } from '../../types'; +import { GlobalState, HostState } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -export function useHostListSelector(selector: (state: HostListState) => TSelected) { +export function useHostSelector(selector: (state: HostState) => TSelected) { return useSelector(function(state: GlobalState) { return selector(state.hostList); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index 88416b577ed0c..11dbed716c527 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -139,7 +139,7 @@ describe('when on the hosts page', () => { expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.textContent).toEqual('Successful'); expect(policyStatusLink.getAttribute('href')).toEqual( - '?selected_host=1&show=policy_response' + '?page_index=0&page_size=10&selected_host=1&show=policy_response' ); }); it('should update the URL when policy status link is clicked', async () => { @@ -150,7 +150,9 @@ describe('when on the hosts page', () => { fireEvent.click(policyStatusLink); }); const changedUrlAction = await userChangedUrlChecker; - expect(changedUrlAction.payload.search).toEqual('?selected_host=1&show=policy_response'); + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_host=1&show=policy_response' + ); }); it('should include the link to logs', async () => { const renderResult = render(); @@ -170,7 +172,7 @@ describe('when on the hosts page', () => { }); }); - it('should navigate to logs without full page refresh', async () => { + it('should navigate to logs without full page refresh', () => { expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); }); }); @@ -205,7 +207,9 @@ describe('when on the hosts page', () => { it('should include the back to details link', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); - expect(subHeaderBackLink.getAttribute('href')).toBe('?selected_host=1'); + expect(subHeaderBackLink.getAttribute('href')).toBe( + '?page_index=0&page_size=10&selected_host=1' + ); }); it('should update URL when back to details link is clicked', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); @@ -214,7 +218,9 @@ describe('when on the hosts page', () => { fireEvent.click(subHeaderBackLink); }); const changedUrlAction = await userChangedUrlChecker; - expect(changedUrlAction.payload.search).toEqual('?selected_host=1'); + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_host=1' + ); }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx index 1d81d6e8a16db..5c2922162ce0c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -4,47 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useMemo, useCallback, memo } from 'react'; +import { EuiHorizontalRule, EuiBasicTable, EuiText, EuiLink, EuiHealth } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageContent, - EuiHorizontalRule, - EuiTitle, - EuiBasicTable, - EuiText, - EuiLink, - EuiHealth, -} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; import { EuiBasicTableColumn } from '@elastic/eui'; import { HostDetailsFlyout } from './details'; import * as selectors from '../../store/hosts/selectors'; -import { HostAction } from '../../store/hosts/action'; -import { useHostListSelector } from './hooks'; +import { useHostSelector } from './hooks'; import { CreateStructuredSelector } from '../../types'; import { urlFromQueryParams } from './url_from_query_params'; import { HostMetadata, Immutable } from '../../../../../common/types'; +import { PageView } from '../components/page_view'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; + +const HostLink = memo<{ + name: string; + href: string; + route: ReturnType; +}>(({ name, href, route }) => { + const clickHandler = useNavigateByRouterEventHandler(route); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {name} + + ); +}); const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const HostList = () => { - const dispatch = useDispatch<(a: HostAction) => void>(); const history = useHistory(); const { listData, pageIndex, pageSize, totalHits: totalItemCount, - isLoading, + listLoading: loading, + listError, uiQueryParams: queryParams, hasSelectedHost, - } = useHostListSelector(selector); + } = useHostSelector(selector); const paginationSetup = useMemo(() => { return { @@ -59,12 +62,15 @@ export const HostList = () => { const onTableChange = useCallback( ({ page }: { page: { index: number; size: number } }) => { const { index, size } = page; - dispatch({ - type: 'userPaginatedHostList', - payload: { pageIndex: index, pageSize: size }, - }); + history.push( + urlFromQueryParams({ + ...queryParams, + page_index: JSON.stringify(index), + page_size: JSON.stringify(size), + }) + ); }, - [dispatch] + [history, queryParams] ); const columns: Array>> = useMemo(() => { @@ -75,18 +81,9 @@ export const HostList = () => { defaultMessage: 'Hostname', }), render: ({ host: { hostname, id } }: { host: { hostname: string; id: string } }) => { + const newQueryParams = urlFromQueryParams({ ...queryParams, selected_host: id }); return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - { - ev.preventDefault(); - history.push(urlFromQueryParams({ ...queryParams, selected_host: id })); - }} - > - {hostname} - + ); }, }, @@ -95,6 +92,7 @@ export const HostList = () => { name: i18n.translate('xpack.endpoint.host.list.policy', { defaultMessage: 'Policy', }), + truncateText: true, render: () => { return 'Policy Name'; }, @@ -129,6 +127,7 @@ export const HostList = () => { name: i18n.translate('xpack.endpoint.host.list.ip', { defaultMessage: 'IP Address', }), + truncateText: true, }, { field: '', @@ -150,62 +149,32 @@ export const HostList = () => { }, }, ]; - }, [queryParams, history]); + }, [queryParams]); return ( - + {hasSelectedHost && } - - - - -

- -

-
-
- - - - - - - [...listData], [listData])} - columns={columns} - loading={isLoading} - pagination={paginationSetup} - onChange={onTableChange} - /> - -
-
-
+ + + + + [...listData], [listData])} + columns={columns} + loading={loading} + error={listError?.message} + pagination={paginationSetup} + onChange={onTableChange} + /> + ); }; - -const HostPage = styled.div` - .hostPage { - padding: 0; - } - .hostHeader { - background-color: ${props => props.theme.eui.euiColorLightestShade}; - border-bottom: ${props => props.theme.eui.euiBorderThin}; - padding: ${props => - props.theme.eui.euiSizeXL + - ' ' + - 0 + - props.theme.eui.euiSizeXL + - ' ' + - props.theme.eui.euiSizeL}; - margin-bottom: 0; - } - .hostPageContent { - border: none; - } -`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx index 2ecc2b117bf01..d780b7bde8af3 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx @@ -101,7 +101,7 @@ describe('Policy Details', () => { 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty' ); expect(history.location.pathname).toEqual('/policy/1'); - backToListButton.simulate('click'); + backToListButton.simulate('click', { button: 0 }); expect(history.location.pathname).toEqual('/policy'); }); it('should display agent stats', async () => { @@ -130,7 +130,7 @@ describe('Policy Details', () => { 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' ); expect(history.location.pathname).toEqual('/policy/1'); - cancelbutton.simulate('click'); + cancelbutton.simulate('click', { button: 0 }); expect(history.location.pathname).toEqual('/policy'); }); it('should display save button', async () => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index 076de7b57b44b..ea9eb292dba1a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -20,7 +20,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { usePolicyDetailsSelector } from './policy_hooks'; import { policyDetails, @@ -36,11 +35,11 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); const { notifications, services } = useKibana(); - const history = useHistory(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -82,13 +81,7 @@ export const PolicyDetails = React.memo(() => { } }, [notifications.toasts, policyItem, policyName, policyUpdateStatus]); - const handleBackToListOnClick: React.MouseEventHandler = useCallback( - ev => { - ev.preventDefault(); - history.push(`/policy`); - }, - [history] - ); + const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy'); const handleSaveOnClick = useCallback(() => { setShowConfirm(true); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index 062c7afb6706d..f7eafff137f51 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -24,30 +24,26 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { PageView } from '../components/page_view'; import { LinkToApp } from '../components/link_to_app'; import { Immutable, PolicyData } from '../../../../../common/types'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; interface TableChangeCallbackArguments { page: { index: number; size: number }; } -const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) => { - const history = useHistory(); - +const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ + name, + route, + href, +}) => { + const clickHandler = useNavigateByRouterEventHandler(route); return ( - { - event.preventDefault(); - history.push(route); - }} - > + // eslint-disable-next-line @elastic/eui/href-or-on-click + {name} ); }; -const renderPolicyNameLink = (value: string, item: Immutable) => { - return ; -}; - export const PolicyList = React.memo(() => { const { services, notifications } = useKibana(); const history = useHistory(); @@ -95,7 +91,16 @@ export const PolicyList = React.memo(() => { name: i18n.translate('xpack.endpoint.policyList.nameField', { defaultMessage: 'Policy Name', }), - render: renderPolicyNameLink, + render: (value: string, item: Immutable) => { + const routeUri = `/policy/${item.id}`; + return ( + + ); + }, truncateText: true, }, { diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts new file mode 100644 index 0000000000000..943a9c22c6aae --- /dev/null +++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts @@ -0,0 +1,14 @@ +/* + * 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 { EndpointAppContextService } from './endpoint_app_context_services'; + +describe('test endpoint app context services', () => { + it('should throw error if start is not called', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(() => endpointAppContextService.getIndexPatternRetriever()).toThrow(Error); + expect(() => endpointAppContextService.getAgentService()).toThrow(Error); + }); +}); diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts new file mode 100644 index 0000000000000..b087405c7bc5b --- /dev/null +++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts @@ -0,0 +1,40 @@ +/* + * 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 { IndexPatternRetriever } from './index_pattern'; +import { AgentService } from '../../ingest_manager/server'; + +/** + * A singleton that holds shared services that are initialized during the start up phase + * of the plugin lifecycle. And stop during the stop phase, if needed. + */ +export class EndpointAppContextService { + private indexPatternRetriever: IndexPatternRetriever | undefined; + private agentService: AgentService | undefined; + + public start(dependencies: { + indexPatternRetriever: IndexPatternRetriever; + agentService: AgentService; + }) { + this.indexPatternRetriever = dependencies.indexPatternRetriever; + this.agentService = dependencies.agentService; + } + + public stop() {} + + public getAgentService(): AgentService { + if (!this.agentService) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.agentService; + } + + public getIndexPatternRetriever(): IndexPatternRetriever { + if (!this.indexPatternRetriever) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.indexPatternRetriever; + } +} diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts index ea612bfd75441..dcedd27fc5a3d 100644 --- a/x-pack/plugins/endpoint/server/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/index_pattern.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server'; -import { ESIndexPatternService } from '../../ingest_manager/server'; import { EndpointAppConstants } from '../common/types'; +import { ESIndexPatternService } from '../../ingest_manager/server'; export interface IndexPatternRetriever { getIndexPattern(ctx: RequestHandlerContext, datasetPath: string): Promise; diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts index 903aa19cd8843..519ca15cf8427 100644 --- a/x-pack/plugins/endpoint/server/mocks.ts +++ b/x-pack/plugins/endpoint/server/mocks.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AgentService, IngestManagerStartContract } from '../../ingest_manager/server'; + /** * Creates a mock IndexPatternRetriever for use in tests. * @@ -28,6 +30,15 @@ export const createMockMetadataIndexPatternRetriever = () => { return createMockIndexPatternRetriever(MetadataIndexPattern); }; +/** + * Creates a mock AgentService + */ +export const createMockAgentService = (): jest.Mocked => { + return { + getAgentStatusById: jest.fn(), + }; +}; + /** * Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's * ESIndexPatternService. @@ -35,10 +46,13 @@ export const createMockMetadataIndexPatternRetriever = () => { * @param indexPattern a string index pattern to return when called by a test * @returns the same value as `indexPattern` parameter */ -export const createMockIndexPatternService = (indexPattern: string) => { +export const createMockIngestManagerStartContract = ( + indexPattern: string +): IngestManagerStartContract => { return { esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), }, + agentService: createMockAgentService(), }; }; diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 8d55e64f16dcf..45e9591a14975 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -4,15 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; +import { + EndpointPlugin, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies, +} from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; -import { createMockIndexPatternService } from './mocks'; +import { createMockIngestManagerStartContract } from './mocks'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; let mockCoreSetup: ReturnType; + let mockCoreStart: ReturnType; let mockedEndpointPluginSetupDependencies: jest.Mocked; + let mockedEndpointPluginStartDependencies: jest.Mocked; let mockedPluginSetupContract: jest.Mocked; beforeEach(() => { plugin = new EndpointPlugin( @@ -23,21 +29,32 @@ describe('test endpoint plugin', () => { ); mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); mockedPluginSetupContract = { registerFeature: jest.fn(), getFeatures: jest.fn(), getFeaturesUICapabilities: jest.fn(), registerLegacyAPI: jest.fn(), }; - mockedEndpointPluginSetupDependencies = { - features: mockedPluginSetupContract, - ingestManager: createMockIndexPatternService(''), - }; }); it('test properly setup plugin', async () => { + mockedEndpointPluginSetupDependencies = { + features: mockedPluginSetupContract, + }; await plugin.setup(mockCoreSetup, mockedEndpointPluginSetupDependencies); expect(mockedPluginSetupContract.registerFeature).toBeCalledTimes(1); expect(mockCoreSetup.http.createRouter).toBeCalledTimes(1); + expect(() => plugin.getEndpointAppContextService().getIndexPatternRetriever()).toThrow(Error); + expect(() => plugin.getEndpointAppContextService().getAgentService()).toThrow(Error); + }); + + it('test properly start plugin', async () => { + mockedEndpointPluginStartDependencies = { + ingestManager: createMockIngestManagerStartContract(''), + }; + await plugin.start(mockCoreStart, mockedEndpointPluginStartDependencies); + expect(plugin.getEndpointAppContextService().getAgentService()).toBeTruthy(); + expect(plugin.getEndpointAppContextService().getIndexPatternRetriever()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index 6a42014e91130..f3cc569ad16a7 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -3,10 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, PluginInitializerContext, Logger } from 'kibana/server'; +import { Plugin, CoreSetup, PluginInitializerContext, Logger, CoreStart } from 'kibana/server'; import { first } from 'rxjs/operators'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; -import { IngestManagerSetupContract } from '../../ingest_manager/server'; import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; @@ -15,14 +14,17 @@ import { registerResolverRoutes } from './routes/resolver'; import { registerIndexPatternRoute } from './routes/index_pattern'; import { registerEndpointRoutes } from './routes/metadata'; import { IngestIndexPatternRetriever } from './index_pattern'; +import { IngestManagerStartContract } from '../../ingest_manager/server'; +import { EndpointAppContextService } from './endpoint_app_context_services'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; -export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface EndpointPluginStartDependencies { + ingestManager: IngestManagerStartContract; +} export interface EndpointPluginSetupDependencies { features: FeaturesPluginSetupContract; - ingestManager: IngestManagerSetupContract; } export class EndpointPlugin @@ -34,9 +36,15 @@ export class EndpointPlugin EndpointPluginStartDependencies > { private readonly logger: Logger; + private readonly endpointAppContextService: EndpointAppContextService = new EndpointAppContextService(); constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get('endpoint'); } + + public getEndpointAppContextService(): EndpointAppContextService { + return this.endpointAppContextService; + } + public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { plugins.features.registerFeature({ id: 'endpoint', @@ -66,11 +74,8 @@ export class EndpointPlugin }, }); const endpointContext = { - indexPatternRetriever: new IngestIndexPatternRetriever( - plugins.ingestManager.esIndexPatternService, - this.initializerContext.logger - ), logFactory: this.initializerContext.logger, + service: this.endpointAppContextService, config: (): Promise => { return createConfig$(this.initializerContext) .pipe(first()) @@ -84,10 +89,18 @@ export class EndpointPlugin registerIndexPatternRoute(router, endpointContext); } - public start() { + public start(core: CoreStart, plugins: EndpointPluginStartDependencies) { this.logger.debug('Starting plugin'); + this.endpointAppContextService.start({ + indexPatternRetriever: new IngestIndexPatternRetriever( + plugins.ingestManager.esIndexPatternService, + this.initializerContext.logger + ), + agentService: plugins.ingestManager.agentService, + }); } public stop() { this.logger.debug('Stopping plugin'); + this.endpointAppContextService.stop(); } } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts index 6be7b26898206..1124c977ff924 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts @@ -12,25 +12,36 @@ import { import { registerAlertRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; -import { createMockIndexPatternRetriever } from '../../mocks'; +import { createMockAgentService, createMockIndexPatternRetriever } from '../../mocks'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('test alerts route', () => { let routerMock: jest.Mocked; let mockClusterClient: jest.Mocked; let mockScopedClient: jest.Mocked; + let endpointAppContextService: EndpointAppContextService; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient(); mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); - registerAlertRoutes(routerMock, { + + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start({ indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'), + agentService: createMockAgentService(), + }); + + registerAlertRoutes(routerMock, { logFactory: loggingServiceMock.create(), + service: endpointAppContextService, config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + afterEach(() => endpointAppContextService.stop()); + it('should fail to validate when `page_size` is not a number', async () => { const validate = () => { alertingIndexGetQuerySchema.validate({ diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index 86e9f55da5697..e438ab853f3b5 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -26,7 +26,9 @@ export const alertDetailsHandlerWrapper = function( id: alertId, })) as GetResponse; - const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const indexPattern = await endpointAppContext.service + .getIndexPatternRetriever() + .getEventIndexPattern(ctx); const config = await endpointAppContext.config(); const pagination: AlertDetailsPagination = new AlertDetailsPagination( @@ -37,7 +39,13 @@ export const alertDetailsHandlerWrapper = function( indexPattern ); - const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern); + const currentHostInfo = await getHostData( + { + endpointAppContext, + requestHandlerContext: ctx, + }, + response._source.host.id + ); return res.ok({ body: { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts index f23dffd13db4f..44a0cf8744a9e 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts @@ -18,7 +18,9 @@ export const alertListHandlerWrapper = function( res ) => { try { - const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const indexPattern = await endpointAppContext.service + .getIndexPatternRetriever() + .getEventIndexPattern(ctx); const reqData = await getRequestData(req, endpointAppContext); const response = await searchESForAlerts( ctx.core.elasticsearch.dataClient, diff --git a/x-pack/plugins/endpoint/server/routes/index_pattern.ts b/x-pack/plugins/endpoint/server/routes/index_pattern.ts index 3b71f6a6957ba..79083f5f05e14 100644 --- a/x-pack/plugins/endpoint/server/routes/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/routes/index_pattern.ts @@ -8,14 +8,14 @@ import { IRouter, Logger, RequestHandler } from 'kibana/server'; import { EndpointAppContext } from '../types'; import { IndexPatternGetParamsResult, EndpointAppConstants } from '../../common/types'; import { indexPatternGetParamsSchema } from '../../common/schema/index_pattern'; -import { IndexPatternRetriever } from '../index_pattern'; function handleIndexPattern( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); return res.ok({ body: { indexPattern: await indexRetriever.getIndexPattern(context, req.params.datasetPath), @@ -37,6 +37,6 @@ export function registerIndexPatternRoute(router: IRouter, endpointAppContext: E validate: { params: indexPatternGetParamsSchema }, options: { authRequired: true }, }, - handleIndexPattern(log, endpointAppContext.indexPatternRetriever) + handleIndexPattern(log, endpointAppContext) ); } diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 883bb88204fd4..99dc4ac9f9e33 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -4,18 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, RequestHandlerContext } from 'kibana/server'; +import { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; -import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; +import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { EndpointAppContext } from '../../types'; +import { AgentStatus } from '../../../../ingest_manager/common/types/models'; interface HitSource { _source: HostMetadata; } +interface MetadataRequestContext { + requestHandlerContext: RequestHandlerContext; + endpointAppContext: EndpointAppContext; +} + +const HOST_STATUS_MAPPING = new Map([ + ['online', HostStatus.ONLINE], + ['offline', HostStatus.OFFLINE], +]); + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.post( { @@ -50,9 +61,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( - context - ); + const index = await endpointAppContext.service + .getIndexPatternRetriever() + .getMetadataIndexPattern(context); const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, @@ -62,7 +73,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp 'search', queryParams )) as SearchResponse; - return res.ok({ body: mapToHostResultList(queryParams, response) }); + return res.ok({ + body: await mapToHostResultList(queryParams, response, { + endpointAppContext, + requestHandlerContext: context, + }), + }); } catch (err) { return res.internalError({ body: err }); } @@ -79,11 +95,13 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( - context + const doc = await getHostData( + { + endpointAppContext, + requestHandlerContext: context, + }, + req.params.id ); - - const doc = await getHostData(context, req.params.id, index); if (doc) { return res.ok({ body: doc }); } @@ -96,12 +114,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp } export async function getHostData( - context: RequestHandlerContext, - id: string, - index: string + metadataRequestContext: MetadataRequestContext, + id: string ): Promise { + const index = await metadataRequestContext.endpointAppContext.service + .getIndexPatternRetriever() + .getMetadataIndexPattern(metadataRequestContext.requestHandlerContext); const query = getESQueryHostMetadataByID(id, index); - const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( + const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.dataClient.callAsCurrentUser( 'search', query )) as SearchResponse; @@ -110,22 +130,25 @@ export async function getHostData( return undefined; } - return enrichHostMetadata(response.hits.hits[0]._source); + return await enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext); } -function mapToHostResultList( +async function mapToHostResultList( queryParams: Record, - searchResponse: SearchResponse -): HostResultList { + searchResponse: SearchResponse, + metadataRequestContext: MetadataRequestContext +): Promise { const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0; if (searchResponse.hits.hits.length > 0) { return { request_page_size: queryParams.size, request_page_index: queryParams.from, - hosts: searchResponse.hits.hits - .map(response => response.inner_hits.most_recent.hits.hits) - .flatMap(data => data as HitSource) - .map(entry => enrichHostMetadata(entry._source)), + hosts: await Promise.all( + searchResponse.hits.hits + .map(response => response.inner_hits.most_recent.hits.hits) + .flatMap(data => data as HitSource) + .map(async entry => enrichHostMetadata(entry._source, metadataRequestContext)) + ), total: totalNumberOfHosts, }; } else { @@ -138,9 +161,45 @@ function mapToHostResultList( } } -function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo { +async function enrichHostMetadata( + hostMetadata: HostMetadata, + metadataRequestContext: MetadataRequestContext +): Promise { + let hostStatus = HostStatus.ERROR; + let elasticAgentId = hostMetadata?.elastic?.agent?.id; + const log = logger(metadataRequestContext.endpointAppContext); + try { + /** + * Get agent status by elastic agent id if available or use the host id. + * https://github.com/elastic/endpoint-app-team/issues/354 + */ + + if (!elasticAgentId) { + elasticAgentId = hostMetadata.host.id; + log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`); + } + + const status = await metadataRequestContext.endpointAppContext.service + .getAgentService() + .getAgentStatusById( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + elasticAgentId + ); + hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR; + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + log.warn(`agent with id ${elasticAgentId} not found`); + } else { + log.error(e); + throw e; + } + } return { metadata: hostMetadata, - host_status: HostStatus.ERROR, + host_status: hostStatus, }; } + +const logger = (endpointAppContext: EndpointAppContext): Logger => { + return endpointAppContext.logFactory.get('metadata'); +}; diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index 9a7d3fb3188a6..8f0c0b4c2efaf 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -25,7 +25,10 @@ import { SearchResponse } from 'elasticsearch'; import { registerEndpointRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; -import { createMockMetadataIndexPatternRetriever } from '../../mocks'; +import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks'; +import { AgentService } from '../../../../ingest_manager/server'; +import Boom from 'boom'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -35,6 +38,8 @@ describe('test endpoint route', () => { let mockSavedObjectClient: jest.Mocked; let routeHandler: RequestHandler; let routeConfig: RouteConfig; + let mockAgentService: jest.Mocked; + let endpointAppContextService: EndpointAppContextService; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -45,13 +50,22 @@ describe('test endpoint route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); - registerEndpointRoutes(routerMock, { + mockAgentService = createMockAgentService(); + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start({ indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + agentService: mockAgentService, + }); + + registerEndpointRoutes(routerMock, { logFactory: loggingServiceMock.create(), + service: endpointAppContextService, config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + afterEach(() => endpointAppContextService.stop()); + function createRouteHandlerContext( dataClient: jest.Mocked, savedObjectsClient: jest.Mocked @@ -83,7 +97,7 @@ describe('test endpoint route', () => { [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; - + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -113,6 +127,8 @@ describe('test endpoint route', () => { ], }, }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve((data as unknown) as SearchResponse) ); @@ -154,6 +170,8 @@ describe('test endpoint route', () => { filter: 'not host.ip:10.140.73.246', }, }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve((data as unknown) as SearchResponse) ); @@ -216,10 +234,10 @@ describe('test endpoint route', () => { }, }) ); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; - await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -233,13 +251,14 @@ describe('test endpoint route', () => { expect(message).toEqual('Endpoint Not Found'); }); - it('should return a single endpoint with status error', async () => { + it('should return a single endpoint with status online', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: (data as any).hits.hits[0]._id }, }); const response: SearchResponse = (data as unknown) as SearchResponse< HostMetadata >; + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -256,6 +275,64 @@ describe('test endpoint route', () => { expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result).toHaveProperty('metadata.endpoint'); + expect(result.host_status).toEqual(HostStatus.ONLINE); + }); + + it('should return a single endpoint with status error when AgentService throw 404', async () => { + const response: SearchResponse = (data as unknown) as SearchResponse< + HostMetadata + >; + + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: response.hits.hits[0]._id }, + }); + + mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { + throw Boom.notFound('Agent not found'); + }); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; + expect(result.host_status).toEqual(HostStatus.ERROR); + }); + + it('should return a single endpoint with status error when status is not offline or online', async () => { + const response: SearchResponse = (data as unknown) as SearchResponse< + HostMetadata + >; + + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: response.hits.hits[0]._id }, + }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts index c8143fbdda1ea..28bac2fa10e0c 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts @@ -6,7 +6,8 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { createMockMetadataIndexPatternRetriever, MetadataIndexPattern } from '../../mocks'; +import { MetadataIndexPattern } from '../../mocks'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -17,8 +18,8 @@ describe('query builder', () => { const query = await kibanaRequestToMetadataListESQuery( mockRequest, { - indexPatternRetriever: createMockMetadataIndexPatternRetriever(), logFactory: loggingServiceMock.create(), + service: new EndpointAppContextService(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, MetadataIndexPattern @@ -68,8 +69,8 @@ describe('query builder', () => { const query = await kibanaRequestToMetadataListESQuery( mockRequest, { - indexPatternRetriever: createMockMetadataIndexPatternRetriever(), logFactory: loggingServiceMock.create(), + service: new EndpointAppContextService(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, MetadataIndexPattern diff --git a/x-pack/plugins/endpoint/server/routes/resolver.ts b/x-pack/plugins/endpoint/server/routes/resolver.ts index 77fcbc87baeb1..a96d431225b15 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver.ts @@ -12,7 +12,6 @@ import { handleLifecycle, validateLifecycle } from './resolver/lifecycle'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); - const indexPatternService = endpointAppContext.indexPatternRetriever; router.get( { @@ -20,7 +19,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateRelatedEvents, options: { authRequired: true }, }, - handleRelatedEvents(log, indexPatternService) + handleRelatedEvents(log, endpointAppContext) ); router.get( @@ -29,7 +28,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateChildren, options: { authRequired: true }, }, - handleChildren(log, indexPatternService) + handleChildren(log, endpointAppContext) ); router.get( @@ -38,6 +37,6 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateLifecycle, options: { authRequired: true }, }, - handleLifecycle(log, indexPatternService) + handleLifecycle(log, endpointAppContext) ); } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/children.ts index 05b8f0b5f8608..c3b19b6c912b6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/children.ts @@ -11,7 +11,7 @@ import { extractEntityID } from './utils/normalize'; import { getPaginationParams } from './utils/pagination'; import { LifecycleQuery } from './queries/lifecycle'; import { ChildrenQuery } from './queries/children'; -import { IndexPatternRetriever } from '../../index_pattern'; +import { EndpointAppContext } from '../../types'; interface ChildrenQueryParams { after?: string; @@ -47,7 +47,7 @@ export const validateChildren = { export function handleChildren( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { const { @@ -55,6 +55,7 @@ export function handleChildren( query: { limit, after, legacyEndpointID }, } = req; try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); const pagination = getPaginationParams(limit, after); const indexPattern = await indexRetriever.getEventIndexPattern(context); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts index 6d155b79651a7..91a4c5d49bc54 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; import { extractParentEntityID } from './utils/normalize'; import { LifecycleQuery } from './queries/lifecycle'; import { ResolverEvent } from '../../../common/types'; -import { IndexPatternRetriever } from '../../index_pattern'; +import { EndpointAppContext } from '../../types'; interface LifecycleQueryParams { ancestors: number; @@ -48,7 +47,7 @@ function getParentEntityID(results: ResolverEvent[]) { export function handleLifecycle( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { const { @@ -56,6 +55,7 @@ export function handleLifecycle( query: { ancestors, legacyEndpointID }, } = req; try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); const ancestorLifecycles = []; const client = context.core.elasticsearch.dataClient; const indexPattern = await indexRetriever.getEventIndexPattern(context); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts index 46e205464f53c..83e111a1e62e6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; import { getPaginationParams } from './utils/pagination'; import { RelatedEventsQuery } from './queries/related_events'; -import { IndexPatternRetriever } from '../../index_pattern'; +import { EndpointAppContext } from '../../types'; interface RelatedEventsQueryParams { after?: string; @@ -44,7 +44,7 @@ export const validateRelatedEvents = { export function handleRelatedEvents( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { const { @@ -52,6 +52,7 @@ export function handleRelatedEvents( query: { limit, after, legacyEndpointID }, } = req; try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); const pagination = getPaginationParams(limit, after); const client = context.core.elasticsearch.dataClient; diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts index 46a23060339f4..dfa5950adba5c 100644 --- a/x-pack/plugins/endpoint/server/types.ts +++ b/x-pack/plugins/endpoint/server/types.ts @@ -5,13 +5,17 @@ */ import { LoggerFactory } from 'kibana/server'; import { EndpointConfigType } from './config'; -import { IndexPatternRetriever } from './index_pattern'; +import { EndpointAppContextService } from './endpoint_app_context_services'; /** * The context for Endpoint apps. */ export interface EndpointAppContext { - indexPatternRetriever: IndexPatternRetriever; logFactory: LoggerFactory; config(): Promise; + + /** + * Object readiness is tied to plugin start method + */ + service: EndpointAppContextService; } diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 986486902c3fa..470123ada48ea 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -42,6 +42,8 @@ describe('indexDocument', () => { }); describe('doesIlmPolicyExist', () => { + // ElasticsearchError can be a bit random in shape, we need an any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any const notFoundError = new Error('Not found') as any; notFoundError.statusCode = 404; @@ -187,6 +189,8 @@ describe('createIndex', () => { }); test(`shouldn't throw when an error of type resource_already_exists_exception is thrown`, async () => { + // ElasticsearchError can be a bit random in shape, we need an any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any const err = new Error('Already exists') as any; err.body = { error: { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 409bb2d00e161..6d5c6b31a637c 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -5,6 +5,7 @@ */ import { reject, isUndefined } from 'lodash'; +import { SearchResponse, Client } from 'elasticsearch'; import { Logger, ClusterClient } from '../../../../../src/core/server'; import { IEvent } from '../types'; import { FindOptionsType } from '../event_log_client'; @@ -33,8 +34,8 @@ export class ClusterClientAdapter { this.clusterClientPromise = opts.clusterClientPromise; } - public async indexDocument(doc: any): Promise { - await this.callEs('index', doc); + public async indexDocument(doc: unknown): Promise { + await this.callEs>('index', doc); } public async doesIlmPolicyExist(policyName: string): Promise { @@ -51,7 +52,7 @@ export class ClusterClientAdapter { return true; } - public async createIlmPolicy(policyName: string, policy: any): Promise { + public async createIlmPolicy(policyName: string, policy: unknown): Promise { const request = { method: 'PUT', path: `_ilm/policy/${policyName}`, @@ -67,21 +68,27 @@ export class ClusterClientAdapter { public async doesIndexTemplateExist(name: string): Promise { let result; try { - result = await this.callEs('indices.existsTemplate', { name }); + result = await this.callEs>( + 'indices.existsTemplate', + { name } + ); } catch (err) { throw new Error(`error checking existance of index template: ${err.message}`); } return result as boolean; } - public async createIndexTemplate(name: string, template: any): Promise { + public async createIndexTemplate(name: string, template: unknown): Promise { const addTemplateParams = { name, create: true, body: template, }; try { - await this.callEs('indices.putTemplate', addTemplateParams); + await this.callEs>( + 'indices.putTemplate', + addTemplateParams + ); } catch (err) { // The error message doesn't have a type attribute we can look to guarantee it's due // to the template already existing (only long message) so we'll check ourselves to see @@ -97,16 +104,19 @@ export class ClusterClientAdapter { public async doesAliasExist(name: string): Promise { let result; try { - result = await this.callEs('indices.existsAlias', { name }); + result = await this.callEs>( + 'indices.existsAlias', + { name } + ); } catch (err) { throw new Error(`error checking existance of initial index: ${err.message}`); } return result as boolean; } - public async createIndex(name: string, body: any = {}): Promise { + public async createIndex(name: string, body: unknown = {}): Promise { try { - await this.callEs('indices.create', { + await this.callEs>('indices.create', { index: name, body, }); @@ -125,12 +135,12 @@ export class ClusterClientAdapter { ): Promise { try { const { - hits: { - hits, - total: { value: total }, - }, - } = await this.callEs('search', { + hits: { hits, total }, + }: SearchResponse = await this.callEs('search', { index, + // The SearchResponse type only supports total as an int, + // so we're forced to explicitly request that it return as an int + rest_total_hits_as_int: true, body: { size: perPage, from: (page - 1) * perPage, @@ -189,7 +199,7 @@ export class ClusterClientAdapter { page, per_page: perPage, total, - data: hits.map((hit: any) => hit._source) as IEvent[], + data: hits.map(hit => hit._source) as IEvent[], }; } catch (err) { throw new Error( @@ -198,13 +208,15 @@ export class ClusterClientAdapter { } } - private async callEs(operation: string, body?: any): Promise { + // We have a common problem typing ES-DSL Queries + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async callEs(operation: string, body?: any) { try { this.debug(`callEs(${operation}) calls:`, body); const clusterClient = await this.clusterClientPromise; const result = await clusterClient.callAsInternalUser(operation, body); this.debug(`callEs(${operation}) result:`, result); - return result; + return result as ESQueryResult; } catch (err) { this.debug(`callEs(${operation}) error:`, { message: err.message, @@ -214,7 +226,7 @@ export class ClusterClientAdapter { } } - private debug(message: string, object?: any) { + private debug(message: string, object?: unknown) { const objectString = object == null ? '' : JSON.stringify(object); this.logger.debug(`esContext: ${message} ${objectString}`); } diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index 982454e671008..a6af209d6d3a0 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -9,7 +9,7 @@ import mappings from '../../generated/mappings.json'; // returns the body of an index template used in an ES indices.putTemplate call export function getIndexTemplate(esNames: EsNames) { - const indexTemplateBody: any = { + const indexTemplateBody = { index_patterns: [esNames.indexPatternWithVersion], settings: { number_of_shards: 1, diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts index 268421235b4b2..7b3d01f3baa89 100644 --- a/x-pack/plugins/event_log/server/es/names.mock.ts +++ b/x-pack/plugins/event_log/server/es/names.mock.ts @@ -10,7 +10,7 @@ const createNamesMock = () => { const mock: jest.Mocked = { base: '.kibana', alias: '.kibana-event-log-8.0.0', - ilmPolicy: '.kibana-event-log-policy', + ilmPolicy: 'kibana-event-log-policy', indexPattern: '.kibana-event-log-*', indexPatternWithVersion: '.kibana-event-log-8.0.0-*', initialIndex: '.kibana-event-log-8.0.0-000001', diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts index baefd756bb1ed..bc6a4c9a52fac 100644 --- a/x-pack/plugins/event_log/server/es/names.test.ts +++ b/x-pack/plugins/event_log/server/es/names.test.ts @@ -23,4 +23,10 @@ describe('getEsNames()', () => { expect(esNames.initialIndex).toEqual(`${base}-event-log-${version}-000001`); expect(esNames.indexTemplate).toEqual(`${base}-event-log-${version}-template`); }); + + test('ilm policy name does not contain dot prefix', () => { + const base = '.XYZ'; + const esNames = getEsNames(base); + expect(esNames.ilmPolicy).toEqual('XYZ-event-log-policy'); + }); }); diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts index d55d02a16fc9a..8cd56a89d3fbe 100644 --- a/x-pack/plugins/event_log/server/es/names.ts +++ b/x-pack/plugins/event_log/server/es/names.ts @@ -22,10 +22,13 @@ export interface EsNames { export function getEsNames(baseName: string): EsNames { const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`; const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`; + const eventLogPolicyName = `${ + baseName.startsWith('.') ? baseName.substring(1) : baseName + }${EVENT_LOG_NAME_SUFFIX}-policy`; return { base: baseName, alias: eventLogNameWithVersion, - ilmPolicy: `${eventLogName}-policy`, + ilmPolicy: `${eventLogPolicyName}`, indexPattern: `${eventLogName}-*`, indexPatternWithVersion: `${eventLogNameWithVersion}-*`, initialIndex: `${eventLogNameWithVersion}-000001`, diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts index 3b250b7462009..43883ea4e384c 100644 --- a/x-pack/plugins/event_log/server/event_log_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_service.test.ts @@ -7,7 +7,7 @@ import { IEventLogConfig } from './types'; import { EventLogService } from './event_log_service'; import { contextMock } from './es/context.mock'; -import { loggingServiceMock } from '../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const systemLogger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index a8d75bc6c2e5a..58dd3ae6eb514 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -7,8 +7,7 @@ import { EventLogClientService } from './event_log_start_service'; import { contextMock } from './es/context.mock'; import { KibanaRequest } from 'kibana/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('./event_log_client'); @@ -41,7 +40,7 @@ describe('EventLogClientService', () => { function fakeRequest(): KibanaRequest { const savedObjectsClient = savedObjectsClientMock.create(); - return { + return ({ headers: {}, getBasePath: () => '', path: '/', @@ -55,5 +54,5 @@ function fakeRequest(): KibanaRequest { }, }, getSavedObjectsClient: () => savedObjectsClient, - } as any; + } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 673bac4f396e1..6a745931420c0 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -9,20 +9,20 @@ import { ECS_VERSION } from './types'; import { EventLogService } from './event_log_service'; import { EsContext } from './es/context'; import { contextMock } from './es/context.mock'; -import { loggerMock, MockedLogger } from '../../../../src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { delay } from './lib/delay'; import { EVENT_LOGGED_PREFIX } from './event_logger'; const KIBANA_SERVER_UUID = '424-24-2424'; describe('EventLogger', () => { - let systemLogger: MockedLogger; + let systemLogger: ReturnType; let esContext: EsContext; let service: IEventLogService; let eventLogger: IEventLogger; beforeEach(() => { - systemLogger = loggerMock.create(); + systemLogger = loggingServiceMock.createLogger(); esContext = contextMock.create(); service = new EventLogService({ esContext, @@ -153,7 +153,10 @@ describe('EventLogger', () => { }); // return the next logged event; throw if not an event -async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1): Promise { +async function waitForLogEvent( + mockLogger: ReturnType, + waitSeconds: number = 1 +): Promise { const result = await waitForLog(mockLogger, waitSeconds); if (typeof result === 'string') throw new Error('expecting an event'); return result; @@ -161,7 +164,7 @@ async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1 // return the next logged message; throw if it is an event async function waitForLogMessage( - mockLogger: MockedLogger, + mockLogger: ReturnType, waitSeconds: number = 1 ): Promise { const result = await waitForLog(mockLogger, waitSeconds); @@ -171,7 +174,7 @@ async function waitForLogMessage( // return the next logged message, if it's an event log entry, parse it async function waitForLog( - mockLogger: MockedLogger, + mockLogger: ReturnType, waitSeconds: number = 1 ): Promise { const intervals = 4; diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index f5149da069953..bcfd7bd45a6f5 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -168,7 +168,7 @@ function indexEventDoc(esContext: EsContext, doc: Doc): void { } // whew, the thing that actually writes the event log document! -async function indexLogEventDoc(esContext: EsContext, doc: any) { +async function indexLogEventDoc(esContext: EsContext, doc: unknown) { esContext.logger.debug(`writing to event log: ${JSON.stringify(doc)}`); await esContext.waitTillReady(); await esContext.esAdapter.indexDocument(doc); @@ -176,6 +176,6 @@ async function indexLogEventDoc(esContext: EsContext, doc: any) { } // TODO: write log entry to a bounded queue buffer -function writeLogEventDocOnError(esContext: EsContext, doc: any) { +function writeLogEventDocOnError(esContext: EsContext, doc: unknown) { esContext.logger.warn(`unable to write event doc: ${JSON.stringify(doc)}`); } diff --git a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts index c1bb6d70879f3..dd6d15a6e4843 100644 --- a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts +++ b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts @@ -5,7 +5,7 @@ */ import { createBoundedQueue } from './bounded_queue'; -import { loggingServiceMock } from '../../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const logger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/lib/ready_signal.test.ts b/x-pack/plugins/event_log/server/lib/ready_signal.test.ts index d4dbb9064a1ba..6f1d92034c06f 100644 --- a/x-pack/plugins/event_log/server/lib/ready_signal.test.ts +++ b/x-pack/plugins/event_log/server/lib/ready_signal.test.ts @@ -16,11 +16,11 @@ describe('ReadySignal', () => { test('works as expected', async done => { let value = 41; - timeoutSet(100, () => { + timeoutSet(100, async () => { expect(value).toBe(41); }); - timeoutSet(250, () => readySignal.signal(42)); + timeoutSet(250, async () => readySignal.signal(42)); timeoutSet(400, async () => { expect(value).toBe(42); @@ -35,6 +35,6 @@ describe('ReadySignal', () => { }); }); -function timeoutSet(ms: number, fn: any) { +function timeoutSet(ms: number, fn: () => Promise): void { setTimeout(fn, ms); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index e5034f599f118..dd83b2cfb03b8 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -120,7 +120,7 @@ export class Plugin implements CorePlugin, + RequestHandler, 'eventLog' > => { return async (context, request) => { diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts index 19933649277aa..2d5e37e870b28 100644 --- a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts @@ -11,9 +11,9 @@ import { IEventLogClient } from '../types'; export function mockHandlerArguments( eventLogClient: IEventLogClient, - req: any, + req: unknown, res?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { return [ ({ eventLog: { @@ -22,7 +22,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest, + req as KibanaRequest, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts index 844a84dc117a9..f47df499d742f 100644 --- a/x-pack/plugins/event_log/server/routes/find.test.ts +++ b/x-pack/plugins/event_log/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; import { eventLogClientMock } from '../event_log_client.mock'; @@ -17,7 +17,7 @@ beforeEach(() => { describe('find', () => { it('finds events with proper parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); @@ -56,7 +56,7 @@ describe('find', () => { }); it('supports optional pagination parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts index cb170e50fb447..f8e1c842ae436 100644 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -31,9 +31,9 @@ export const findRoute = (router: IRouter) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, FindOptionsType, any, any>, + req: KibanaRequest, FindOptionsType, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { if (!context.eventLog) { return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' }); } diff --git a/x-pack/plugins/graph/public/_main.scss b/x-pack/plugins/graph/public/_main.scss index 2559b7d1aba5c..0d17015385292 100644 --- a/x-pack/plugins/graph/public/_main.scss +++ b/x-pack/plugins/graph/public/_main.scss @@ -25,3 +25,9 @@ -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } + +.gphAppWrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/x-pack/plugins/graph/public/angular/templates/_graph.scss b/x-pack/plugins/graph/public/angular/templates/_graph.scss index e6bd4693d1e9b..4ba65e7ec6b96 100644 --- a/x-pack/plugins/graph/public/angular/templates/_graph.scss +++ b/x-pack/plugins/graph/public/angular/templates/_graph.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/chrome/variables'; - @mixin gphSvgText() { font-family: $euiFontFamily; font-size: $euiSizeS; diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index f804265f1f5ab..35ec0bb2bf6ce 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -12,6 +12,8 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import '../../../../webpackShims/ace'; // required for i18nIdDirective import 'angular-sanitize'; +// required for ngRoute +import 'angular-route'; // type imports import { AppMountContext, @@ -94,7 +96,7 @@ export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) }; }; -const mainTemplate = (basePath: string) => `
+const mainTemplate = (basePath: string) => `
`; @@ -105,14 +107,14 @@ const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.boo function mountGraphApp(appBasePath: string, element: HTMLElement) { const mountpoint = document.createElement('div'); - mountpoint.setAttribute('class', 'kbnLocalApplicationWrapper'); + mountpoint.setAttribute('class', 'gphAppWrapper'); // eslint-disable-next-line mountpoint.innerHTML = mainTemplate(appBasePath); // bootstrap angular into detached element and attach it later to // make angular-within-angular possible const $injector = angular.bootstrap(mountpoint, [moduleName]); element.appendChild(mountpoint); - element.setAttribute('class', 'kbnLocalApplicationWrapper'); + element.setAttribute('class', 'gphAppWrapper'); return $injector; } diff --git a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx index 78e4180aa2b2a..211458e67d05b 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx @@ -129,7 +129,7 @@ export function FieldEditor({ })} onClickAriaLabel={badgeDescription} title="" - onClick={e => { + onClick={(e: React.MouseEvent) => { if (e.shiftKey) { toggleDisabledState(); } else { diff --git a/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx b/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx index 948f89705a5d5..ac656ebdd9512 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx @@ -252,7 +252,11 @@ describe('field_manager', () => { act(() => { getDisplayForm() .find(EuiColorPicker) - .prop('onChange')!('#aaa'); + .prop('onChange')!('#aaa', { + rgba: [170, 170, 170, 1], + hex: '#aaa', + isValid: true, + }); }); fieldEditor.update(); getDisplayForm() diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index a169953d5a10b..141d5d0ea8db4 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -13,6 +13,7 @@ import { registerExploreRoute } from './routes/explore'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { registerSampleData } from './sample_data'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; @@ -32,6 +33,7 @@ export class GraphPlugin implements Plugin { const licenseState = new LicenseState(); licenseState.start(licensing.license$); this.licenseState = licenseState; + core.savedObjects.registerType(graphWorkspace); if (home) { registerSampleData(home.sampleData, licenseState); diff --git a/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts new file mode 100644 index 0000000000000..8e8cb64aac1b9 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsType } from 'kibana/server'; +import { graphMigrations } from './migrations'; + +export const graphWorkspace: SavedObjectsType = { + name: 'graph-workspace', + namespaceType: 'single', + hidden: false, + migrations: graphMigrations, + mappings: { + properties: { + description: { + type: 'text', + }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { + type: 'text', + }, + }, + }, + numLinks: { + type: 'integer', + }, + numVertices: { + type: 'integer', + }, + title: { + type: 'text', + }, + version: { + type: 'integer', + }, + wsState: { + type: 'text', + }, + }, + }, +}; diff --git a/x-pack/plugins/graph/server/saved_objects/index.ts b/x-pack/plugins/graph/server/saved_objects/index.ts new file mode 100644 index 0000000000000..67d1501950175 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { graphWorkspace } from './graph_workspace'; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.test.ts b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..ecf1f3ca3b69e --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { graphMigrations } from './migrations'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +describe('graph-workspace', () => { + describe('7.0.0', () => { + const migration = graphMigrations['7.0.0']; + + test('returns doc on empty object', () => { + expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` + Object { + "references": Array [], + } + `); + }); + + test('returns doc when wsState is not a string', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: true, + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": true, + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when wsState is not valid JSON', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: '123abc', + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "123abc", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when "indexPattern" is missing from wsState', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true })), + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('extract "indexPattern" attribute from doc', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), + bar: true, + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "bar": true, + "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "indexPattern_0", + "type": "index-pattern", + }, + ], + "type": "graph-workspace", + } + `); + }); + }); +}); diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..e77d2ea0fb7c9 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -0,0 +1,40 @@ +/* + * 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 { get } from 'lodash'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +export const graphMigrations = { + '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { + // Set new "references" attribute + doc.references = doc.references || []; + // Migrate index pattern + const wsState = get(doc, 'attributes.wsState'); + if (typeof wsState !== 'string') { + return doc; + } + let state; + try { + state = JSON.parse(JSON.parse(wsState)); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + const { indexPattern } = state; + if (!indexPattern) { + return doc; + } + state.indexPatternRefName = 'indexPattern_0'; + delete state.indexPattern; + doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); + doc.references.push({ + name: 'indexPattern_0', + type: 'index-pattern', + id: indexPattern, + }); + return doc; + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts index 7d24bc31006b4..aa3ac9ea75c22 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export let toasts: any; -export let fatalErrors: any; +import { IToasts, FatalErrorsSetup } from 'src/core/public'; -export function init(_toasts: any, _fatalErrors: any): void { +export let toasts: IToasts; +export let fatalErrors: FatalErrorsSetup; + +export function init(_toasts: IToasts, _fatalErrors: FatalErrorsSetup): void { toasts = _toasts; fatalErrors = _fatalErrors; } diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx index 68bfe41fd538e..9494e4aa396a5 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx @@ -6,19 +6,19 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; export const Layout = withTheme(({ metrics, theme, onChangeRangeTime }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx index 683851fec4d77..b2da7dec3f2e0 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx @@ -6,9 +6,9 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; export const AwsEC2ToolbarItems = (props: ToolbarProps) => { diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx index 220c6c67f4aea..08b865f01b06c 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx @@ -6,17 +6,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx index 24f05fd91589c..2a8394b9dd3a4 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx @@ -6,9 +6,9 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; export const AwsRDSToolbarItems = (props: ToolbarProps) => { diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx index 805236cf47082..e16f8ef6addde 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx @@ -6,17 +6,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx index 54c3196ae2833..324bdd0586029 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx @@ -6,9 +6,9 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; export const AwsS3ToolbarItems = (props: ToolbarProps) => { diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx index d581ac751682d..ff13f2db104de 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx @@ -6,17 +6,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx index 22cb2740a4a7f..3229c07034772 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx @@ -6,9 +6,9 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; export const AwsSQSToolbarItems = (props: ToolbarProps) => { diff --git a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx index 9956b2c9a2ce4..b9366a43e40c6 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx @@ -6,21 +6,21 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx index dbb77cb3b677e..f6c707726d9ca 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx @@ -6,8 +6,8 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; export const ContainerToolbarItems = (props: ToolbarProps) => { diff --git a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx index 6d7d361254220..e23118c747a9b 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx @@ -8,21 +8,21 @@ import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; import * as Aws from '../shared/layouts/aws'; import * as Ngnix from '../shared/layouts/nginx'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx index fc7696ee53c9d..136264c0e26f4 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx @@ -6,8 +6,8 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; export const HostToolbarItems = (props: ToolbarProps) => { diff --git a/x-pack/plugins/infra/common/inventory_models/layouts.ts b/x-pack/plugins/infra/common/inventory_models/layouts.ts index b59ce010361ec..df82f154b47b3 100644 --- a/x-pack/plugins/infra/common/inventory_models/layouts.ts +++ b/x-pack/plugins/infra/common/inventory_models/layouts.ts @@ -23,7 +23,7 @@ import { Layout as AwsRDSLayout } from './aws_rds/layout'; import { Layout as AwsSQSLayout } from './aws_sqs/layout'; import { InventoryItemType } from './types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutProps } from '../../public/pages/metrics/types'; +import { LayoutProps } from '../../public/pages/metrics/metric_detail/types'; interface Layouts { [type: string]: ReactNode; diff --git a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx index 8bc2f3ee8b4b3..271e32556ae28 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx @@ -6,22 +6,22 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; import * as Nginx from '../shared/layouts/nginx'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( diff --git a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx index d48c27efd88fd..c1cd375ff47bf 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx @@ -6,8 +6,8 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; export const PodToolbarItems = (props: ToolbarProps) => { diff --git a/x-pack/plugins/infra/common/inventory_models/shared/compontents/cloud_toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/shared/components/cloud_toolbar_items.tsx similarity index 75% rename from x-pack/plugins/infra/common/inventory_models/shared/compontents/cloud_toolbar_items.tsx rename to x-pack/plugins/infra/common/inventory_models/shared/components/cloud_toolbar_items.tsx index 766a8ae8142f5..da5017b0f3a36 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/compontents/cloud_toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/components/cloud_toolbar_items.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../../public/components/inventory/toolbars/toolbar'; +import { ToolbarProps } from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleAccountsControls } from '../../../../public/components/waffle/waffle_accounts_controls'; +import { WaffleAccountsControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleRegionControls } from '../../../../public/components/waffle/waffle_region_controls'; +import { WaffleRegionControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_region_controls'; type Props = ToolbarProps; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx similarity index 81% rename from x-pack/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx rename to x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx index 738fce45ee99f..bf37828ed0856 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx @@ -7,16 +7,16 @@ import React, { useMemo } from 'react'; import { EuiFlexItem } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../../public/components/inventory/toolbars/toolbar'; +import { ToolbarProps } from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleMetricControls } from '../../../../public/components/waffle/metric_control'; +import { WaffleMetricControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/metric_control'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleGroupByControls } from '../../../../public/components/waffle/waffle_group_by_controls'; +import { WaffleGroupByControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls'; import { toGroupByOpt, toMetricOpt, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../public/components/inventory/toolbars/toolbar_wrapper'; +} from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper'; import { SnapshotMetricType } from '../../types'; interface Props extends ToolbarProps { diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx index 7a0b898d406ce..6f2791534c17e 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx @@ -6,15 +6,15 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../../public/pages/metrics/components/section'; +import { Section } from '../../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../../observability/public'; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx index 79cea5150d498..cf3a06994cc96 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../../public/pages/metrics/components/section'; +import { Section } from '../../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../../observability/public'; diff --git a/x-pack/plugins/infra/common/inventory_models/toolbars.ts b/x-pack/plugins/infra/common/inventory_models/toolbars.ts index 39e9f5a260f7a..e00cb094d0455 100644 --- a/x-pack/plugins/infra/common/inventory_models/toolbars.ts +++ b/x-pack/plugins/infra/common/inventory_models/toolbars.ts @@ -11,7 +11,7 @@ import { HostToolbarItems } from './host/toolbar_items'; import { ContainerToolbarItems } from './container/toolbar_items'; import { PodToolbarItems } from './pod/toolbar_items'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../public/components/inventory/toolbars/toolbar'; +import { ToolbarProps } from '../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; import { AwsEC2ToolbarItems } from './aws_ec2/toolbar_items'; import { AwsS3ToolbarItems } from './aws_s3/toolbar_items'; import { AwsRDSToolbarItems } from './aws_rds/toolbar_items'; diff --git a/x-pack/plugins/infra/common/saved_objects/inventory_view.ts b/x-pack/plugins/infra/common/saved_objects/inventory_view.ts index 8ae765f379add..8933de57b0448 100644 --- a/x-pack/plugins/infra/common/saved_objects/inventory_view.ts +++ b/x-pack/plugins/infra/common/saved_objects/inventory_view.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ElasticsearchMappingOf } from '../../server/utils/typed_elasticsearch_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleViewState } from '../../public/pages/inventory_view/hooks/use_waffle_view_state'; +import { WaffleViewState } from '../../public/pages/metrics/inventory_view/hooks/use_waffle_view_state'; export const inventoryViewSavedObjectType = 'inventory-view'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts index a9f1194844640..6eb08eabc15b5 100644 --- a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts +++ b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts @@ -11,7 +11,7 @@ import { MetricsExplorerChartOptions, MetricsExplorerTimeOptions, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../public/containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedViewSavedObject } from '../../public/hooks/use_saved_view'; diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index ebf9562c38d7a..4c213700b62e6 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createBrowserHistory } from 'history'; import React from 'react'; import ReactDOM from 'react-dom'; import { ApolloProvider } from 'react-apollo'; @@ -25,6 +24,7 @@ import { AppRouter } from '../routers'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; import '../index.scss'; +import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt'; export const CONTAINER_CLASSNAME = 'infra-container-element'; @@ -36,8 +36,8 @@ export async function startApp( Router: AppRouter, triggersActionsUI: TriggersAndActionsUIPublicPluginSetup ) { - const { element, appBasePath } = params; - const history = createBrowserHistory({ basename: appBasePath }); + const { element, history } = params; + const InfraPluginRoot: React.FunctionComponent = () => { const [darkMode] = useUiSetting$('theme:darkMode'); @@ -49,7 +49,9 @@ export async function startApp( - + + + diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx index 914054e1fd9b7..38709c117c817 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx @@ -10,8 +10,8 @@ import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; -import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; interface Props { visible?: boolean; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 0e9da32aaa509..cdefffeb35c15 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { MetricExpressionParams, Comparator, + Aggregators, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../server/lib/alerting/metric_threshold/types'; import { euiStyled } from '../../../../../observability/public'; @@ -31,14 +32,16 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { MetricsExplorerKueryBar } from '../../metrics_explorer/kuery_bar'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by'; +import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; interface AlertContextMeta { @@ -64,24 +67,25 @@ type MetricExpression = Omit & { metric?: string; }; -enum AGGREGATION_TYPES { - COUNT = 'count', - AVERAGE = 'avg', - SUM = 'sum', - MIN = 'min', - MAX = 'max', - RATE = 'rate', - CARDINALITY = 'cardinality', -} - const defaultExpression = { - aggType: AGGREGATION_TYPES.AVERAGE, + aggType: Aggregators.AVERAGE, comparator: Comparator.GT, threshold: [], timeSize: 1, timeUnit: 'm', } as MetricExpression; +const customComparators = { + ...builtInComparators, + [Comparator.OUTSIDE_RANGE]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { + defaultMessage: 'Is not between', + }), + value: Comparator.OUTSIDE_RANGE, + requiredValues: 2, + }, +}; + export const Expressions: React.FC = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ @@ -339,7 +343,7 @@ const StyledExpression = euiStyled.div` export const ExpressionRow: React.FC = props => { const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props; const { - aggType = AGGREGATION_TYPES.MAX, + aggType = Aggregators.MAX, metric, comparator = Comparator.GT, threshold = [], @@ -410,6 +414,7 @@ export const ExpressionRow: React.FC = props => { { - const { sourceId, source } = useSourceContext(); - const { - metric, - groupBy, - nodeType, - accountId, - region, - changeView, - view, - autoBounds, - boundsOverride, - } = useWaffleOptionsContext(); - const { accounts, regions } = useInventoryMeta(sourceId, nodeType); - const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); - const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); - const { loading, nodes, reload, interval } = useSnapshot( - filterQueryAsJson, - metric, - groupBy, - nodeType, - sourceId, - currentTime, - accountId, - region - ); - - const options = { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - legend: { - type: 'gradient', - rules: [ - { value: 0, color: '#D3DAE6' }, - { value: 1, color: euiVisColorPalette[1] }, - ], - } as InfraWaffleMapGradientLegend, - metric, - fields: source?.configuration?.fields, - groupBy, - }; - - useInterval( - () => { - if (!loading) { - jumpToTime(Date.now()); - } - }, - isAutoReloading ? 5000 : null - ); - - return ( - <> - - - - - - ); -}; diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx b/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx deleted file mode 100644 index cb315d3e17b03..0000000000000 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx +++ /dev/null @@ -1,21 +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 React from 'react'; -import { SavedViewsToolbarControls } from '../../saved_views/toolbar_control'; -import { inventoryViewSavedObjectType } from '../../../../common/saved_objects/inventory_view'; -import { useWaffleViewState } from '../../../pages/inventory_view/hooks/use_waffle_view_state'; - -export const SavedViews = () => { - const { viewState, defaultViewState, onViewChange } = useWaffleViewState(); - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx deleted file mode 100644 index 63ab6d2f4465a..0000000000000 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx +++ /dev/null @@ -1,65 +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 React, { FunctionComponent } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; -import { - SnapshotMetricInput, - SnapshotGroupBy, - SnapshotCustomMetricInput, -} from '../../../../common/http_api/snapshot_api'; -import { InventoryCloudAccount } from '../../../../common/http_api/inventory_meta_api'; -import { findToolbar } from '../../../../common/inventory_models/toolbars'; -import { ToolbarWrapper } from './toolbar_wrapper'; - -import { InfraGroupByOptions } from '../../../lib/lib'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { WaffleOptionsState } from '../../../pages/inventory_view/hooks/use_waffle_options'; -import { SavedViews } from './save_views'; - -export interface ToolbarProps - extends Omit { - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; - changeMetric: (payload: SnapshotMetricInput) => void; - changeGroupBy: (payload: SnapshotGroupBy) => void; - changeCustomOptions: (payload: InfraGroupByOptions[]) => void; - changeAccount: (id: string) => void; - changeRegion: (name: string) => void; - accounts: InventoryCloudAccount[]; - regions: string[]; - changeCustomMetrics: (payload: SnapshotCustomMetricInput[]) => void; -} - -const wrapToolbarItems = ( - ToolbarItems: FunctionComponent, - accounts: InventoryCloudAccount[], - regions: string[] -) => { - return ( - - {props => ( - <> - - - - - - - )} - - ); -}; - -interface Props { - nodeType: InventoryItemType; - regions: string[]; - accounts: InventoryCloudAccount[]; -} -export const Toolbar = ({ nodeType, accounts, regions }: Props) => { - const ToolbarItems = findToolbar(nodeType); - return wrapToolbarItems(ToolbarItems, accounts, regions); -}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/series_chart.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/series_chart.tsx deleted file mode 100644 index ed7a994dd2bbe..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/series_chart.tsx +++ /dev/null @@ -1,107 +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 React from 'react'; -import { - ScaleType, - AreaSeries, - BarSeries, - RecursivePartial, - AreaSeriesStyle, - BarSeriesStyle, -} from '@elastic/charts'; -import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; -import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; -import { createMetricLabel } from './helpers/create_metric_label'; -import { - MetricsExplorerOptionsMetric, - MetricsExplorerChartType, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; - -interface Props { - metric: MetricsExplorerOptionsMetric; - id: string | number; - series: MetricsExplorerSeries; - type: MetricsExplorerChartType; - stack: boolean; -} - -export const MetricExplorerSeriesChart = (props: Props) => { - if (MetricsExplorerChartType.bar === props.type) { - return ; - } - return ; -}; - -export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Props) => { - const color = - (metric.color && colorTransformer(metric.color)) || - colorTransformer(MetricsExplorerColor.color0); - - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; - - const seriesAreaStyle: RecursivePartial = { - line: { - strokeWidth: 2, - visible: true, - }, - area: { - opacity: 0.5, - visible: type === MetricsExplorerChartType.area, - }, - }; - return ( - - ); -}; - -export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => { - const color = - (metric.color && colorTransformer(metric.color)) || - colorTransformer(MetricsExplorerColor.color0); - - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; - - const seriesBarStyle: RecursivePartial = { - rectBorder: { - stroke: color, - strokeWidth: 1, - visible: true, - }, - rect: { - opacity: 1, - }, - }; - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx deleted file mode 100644 index 0fbb0b6acad17..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx +++ /dev/null @@ -1,148 +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 { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; -import { - MetricsExplorerMetric, - MetricsExplorerAggregation, -} from '../../../common/http_api/metrics_explorer'; -import { - MetricsExplorerOptions, - MetricsExplorerTimeOptions, - MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { Toolbar } from '../eui/toolbar'; -import { MetricsExplorerKueryBar } from './kuery_bar'; -import { MetricsExplorerMetrics } from './metrics'; -import { MetricsExplorerGroupBy } from './group_by'; -import { MetricsExplorerAggregationPicker } from './aggregation'; -import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options'; -import { SavedViewsToolbarControls } from '../saved_views/toolbar_control'; -import { MetricExplorerViewState } from '../../pages/infrastructure/metrics_explorer/use_metric_explorer_state'; -import { metricsExplorerViewSavedObjectType } from '../../../common/saved_objects/metrics_explorer_view'; -import { useKibanaUiSetting } from '../../utils/use_kibana_ui_setting'; -import { mapKibanaQuickRangesToDatePickerRanges } from '../../utils/map_timepicker_quickranges_to_datepicker_ranges'; - -interface Props { - derivedIndexPattern: IIndexPattern; - timeRange: MetricsExplorerTimeOptions; - options: MetricsExplorerOptions; - chartOptions: MetricsExplorerChartOptions; - defaultViewState: MetricExplorerViewState; - onRefresh: () => void; - onTimeChange: (start: string, end: string) => void; - onGroupByChange: (groupBy: string | null) => void; - onFilterQuerySubmit: (query: string) => void; - onMetricsChange: (metrics: MetricsExplorerMetric[]) => void; - onAggregationChange: (aggregation: MetricsExplorerAggregation) => void; - onChartOptionsChange: (chartOptions: MetricsExplorerChartOptions) => void; - onViewStateChange: (vs: MetricExplorerViewState) => void; -} - -export const MetricsExplorerToolbar = ({ - timeRange, - derivedIndexPattern, - options, - onTimeChange, - onRefresh, - onGroupByChange, - onFilterQuerySubmit, - onMetricsChange, - onAggregationChange, - chartOptions, - onChartOptionsChange, - defaultViewState, - onViewStateChange, -}: Props) => { - const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0; - const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); - const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); - - return ( - - - - - - {options.aggregation !== 'count' && ( - - - - )} - {options.aggregation !== 'count' && ( - - - - )} - - - - - - - - - - - - - - - - - - - - onTimeChange(start, end)} - onRefresh={onRefresh} - commonlyUsedRanges={commonlyUsedRanges} - /> - - - - ); -}; diff --git a/x-pack/plugins/infra/public/components/nodes_overview/index.tsx b/x-pack/plugins/infra/public/components/nodes_overview/index.tsx deleted file mode 100644 index ef22e0486f892..0000000000000 --- a/x-pack/plugins/infra/public/components/nodes_overview/index.tsx +++ /dev/null @@ -1,260 +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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { get, max, min } from 'lodash'; -import React from 'react'; - -import { euiStyled } from '../../../../observability/public'; -import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; -import { createFormatter } from '../../utils/formatters'; -import { NoData } from '../empty_states'; -import { InfraLoadingPanel } from '../loading'; -import { Map } from '../waffle/map'; -import { ViewSwitcher } from '../waffle/view_switcher'; -import { TableView } from './table'; -import { SnapshotNode, SnapshotCustomMetricInputRT } from '../../../common/http_api/snapshot_api'; -import { convertIntervalToString } from '../../utils/convert_interval_to_string'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { createFormatterForMetric } from '../metrics_explorer/helpers/create_formatter_for_metric'; - -export interface KueryFilterQuery { - kind: 'kuery'; - expression: string; -} - -interface Props { - options: InfraWaffleMapOptions; - nodeType: InventoryItemType; - nodes: SnapshotNode[]; - loading: boolean; - reload: () => void; - onDrilldown: (filter: KueryFilterQuery) => void; - currentTime: number; - onViewChange: (view: string) => void; - view: string; - boundsOverride: InfraWaffleMapBounds; - autoBounds: boolean; - interval: string; -} - -interface MetricFormatter { - formatter: InfraFormatterType; - template: string; - bounds?: { min: number; max: number }; -} - -interface MetricFormatters { - [key: string]: MetricFormatter; -} - -const METRIC_FORMATTERS: MetricFormatters = { - ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, - ['cpu']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['memory']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['logRate']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}/s', - }, - ['diskIOReadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['diskIOWriteBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['s3BucketSize']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3TotalRequests']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3NumberOfObjects']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3UploadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3DownloadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['sqsOldestMessage']: { - formatter: InfraFormatterType.number, - template: '{{value}} seconds', - }, -}; - -const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { - const maxValues = nodes.map(node => node.metric.max); - const minValues = nodes.map(node => node.metric.value); - // if there is only one value then we need to set the bottom range to zero for min - // otherwise the legend will look silly since both values are the same for top and - // bottom. - if (minValues.length === 1) { - minValues.unshift(0); - } - return { min: min(minValues) || 0, max: max(maxValues) || 0 }; -}; - -export const NodesOverview = class extends React.Component { - public static displayName = 'Waffle'; - public render() { - const { - autoBounds, - boundsOverride, - loading, - nodes, - nodeType, - reload, - view, - currentTime, - options, - interval, - } = this.props; - if (loading) { - return ( - - ); - } else if (!loading && nodes && nodes.length === 0) { - return ( - { - reload(); - }} - testString="noMetricsDataPrompt" - /> - ); - } - const dataBounds = calculateBoundsFromNodes(nodes); - const bounds = autoBounds ? dataBounds : boundsOverride; - const intervalAsString = convertIntervalToString(interval); - return ( - - - - - - - - -

- -

-
-
-
-
- {view === 'table' ? ( - - - - ) : ( - - - - )} -
- ); - } - - private handleViewChange = (view: string) => this.props.onViewChange(view); - - // TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example. - private formatter = (val: string | number) => { - const { metric } = this.props.options; - if (SnapshotCustomMetricInputRT.is(metric)) { - const formatter = createFormatterForMetric(metric); - return formatter(val); - } - const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); - if (val == null) { - return ''; - } - const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); - return formatter(val); - }; - - private handleDrilldown = (filter: string) => { - this.props.onDrilldown({ - kind: 'kuery', - expression: filter, - }); - return; - }; -}; - -const MainContainer = euiStyled.div` - position: relative; - flex: 1 1 auto; -`; - -const TableContainer = euiStyled.div` - padding: ${props => props.theme.eui.paddingSizes.l}; -`; - -const ViewSwitcherContainer = euiStyled.div` - padding: ${props => props.theme.eui.paddingSizes.l}; -`; - -const MapContainer = euiStyled.div` - position: absolute; - display: flex; - top: 70px; - right: 0; - bottom: 0; - left: 0; -`; diff --git a/x-pack/plugins/infra/public/components/nodes_overview/table.tsx b/x-pack/plugins/infra/public/components/nodes_overview/table.tsx deleted file mode 100644 index 82991076255ee..0000000000000 --- a/x-pack/plugins/infra/public/components/nodes_overview/table.tsx +++ /dev/null @@ -1,170 +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 { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { last } from 'lodash'; -import React, { useState, useCallback, useEffect } from 'react'; -import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap'; -import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; -import { fieldToName } from '../waffle/lib/field_to_display_name'; -import { NodeContextMenu } from '../waffle/node_context_menu'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { SnapshotNode, SnapshotNodePath } from '../../../common/http_api/snapshot_api'; -import { CONTAINER_CLASSNAME } from '../../apps/start_app'; - -interface Props { - nodes: SnapshotNode[]; - nodeType: InventoryItemType; - options: InfraWaffleMapOptions; - currentTime: number; - formatter: (subject: string | number) => string; - onFilter: (filter: string) => void; -} - -const getGroupPaths = (path: SnapshotNodePath[]) => { - switch (path.length) { - case 3: - return path.slice(0, 2); - case 2: - return path.slice(0, 1); - default: - return []; - } -}; - -export const TableView = (props: Props) => { - const { nodes, options, formatter, currentTime, nodeType } = props; - const [openPopovers, setOpenPopovers] = useState([]); - const openPopoverFor = useCallback( - (id: string) => () => { - setOpenPopovers([...openPopovers, id]); - }, - [openPopovers] - ); - - const closePopoverFor = useCallback( - (id: string) => () => { - if (openPopovers.includes(id)) { - setOpenPopovers(openPopovers.filter(subject => subject !== id)); - } - }, - [openPopovers] - ); - - useEffect(() => { - const el = document.getElementsByClassName(CONTAINER_CLASSNAME)[0]; - if (el instanceof HTMLElement) { - if (openPopovers.length > 0) { - el.style.overflowY = 'hidden'; - } else { - el.style.overflowY = 'auto'; - } - } - }, [openPopovers]); - - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }), - sortable: true, - truncateText: true, - textOnly: true, - render: (value: string, item: { node: InfraWaffleMapNode }) => { - const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`; - // For the table we need to create a UniqueID that takes into to account the groupings - // as well as the node name. There is the possibility that a node can be present in two - // different groups and be on the screen at the same time. - const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':'); - return ( - - - {value} - - - ); - }, - }, - ...options.groupBy.map((grouping, index) => ({ - field: `group_${index}`, - name: fieldToName((grouping && grouping.field) || ''), - sortable: true, - truncateText: true, - textOnly: true, - render: (value: string) => { - const handleClick = () => props.onFilter(`${grouping.field}:"${value}"`); - return ( - - {value} - - ); - }, - })), - { - field: 'value', - name: i18n.translate('xpack.infra.tableView.columnName.last1m', { - defaultMessage: 'Last 1m', - }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => {formatter(value)}, - }, - { - field: 'avg', - name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => {formatter(value)}, - }, - { - field: 'max', - name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => {formatter(value)}, - }, - ]; - - const items = nodes.map(node => { - const name = last(node.path); - return { - name: (name && name.label) || 'unknown', - ...getGroupPaths(node.path).reduce( - (acc, path, index) => ({ - ...acc, - [`group_${index}`]: path.label, - }), - {} - ), - value: node.metric.value, - avg: node.metric.avg, - max: node.metric.max, - node: createWaffleMapNode(node), - }; - }); - const initialSorting = { - sort: { - field: 'value', - direction: 'desc', - }, - } as const; - - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 36645fa3f1f35..7f248cd103003 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -17,7 +17,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useContext, useMemo } from 'react'; -import { Prompt } from 'react-router-dom'; import { Source } from '../../containers/source'; import { FieldsConfigurationPanel } from './fields_configuration_panel'; @@ -26,6 +25,7 @@ import { NameConfigurationPanel } from './name_configuration_panel'; import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; import { SourceLoadingPage } from '../source_loading_page'; +import { Prompt } from '../../utils/navigation_warning_prompt'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; @@ -100,10 +100,13 @@ export const SourceConfigurationSettings = ({ data-test-subj="sourceConfigurationContent" > ({ + grow: false, + paddingSize: 'none', +}))` + border-top: none; + border-right: none; + border-left: none; + border-radius: 0; + padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`}; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts b/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts deleted file mode 100644 index f793afee1b948..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts +++ /dev/null @@ -1,17 +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 { InfraWaffleMapGradientLegend, InfraWaffleMapStepLegend } from '../../../lib/lib'; - -export function isInfraWaffleMapStepLegend(subject: any): subject is InfraWaffleMapStepLegend { - return subject.type && subject.type === 'step'; -} - -export function isInfraWaffleMapGradientLegend( - subject: any -): subject is InfraWaffleMapGradientLegend { - return subject.type && subject.type === 'gradient'; -} diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/components/waffle/metric_control/index.tsx deleted file mode 100644 index 0f2034fe9cb25..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/index.tsx +++ /dev/null @@ -1,199 +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 { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState, useCallback } from 'react'; -import { IFieldType } from 'src/plugins/data/public'; -import { - SnapshotMetricInput, - SnapshotCustomMetricInput, - SnapshotCustomMetricInputRT, -} from '../../../../common/http_api/snapshot_api'; -import { CustomMetricForm } from './custom_metric_form'; -import { getCustomMetricLabel } from './get_custom_metric_label'; -import { MetricsContextMenu } from './metrics_context_menu'; -import { ModeSwitcher } from './mode_switcher'; -import { MetricsEditMode } from './metrics_edit_mode'; -import { CustomMetricMode } from './types'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; - -interface Props { - options: Array<{ text: string; value: string }>; - metric: SnapshotMetricInput; - fields: IFieldType[]; - onChange: (metric: SnapshotMetricInput) => void; - onChangeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void; - customMetrics: SnapshotCustomMetricInput[]; -} - -export const WaffleMetricControls = ({ - fields, - onChange, - onChangeCustomMetrics, - metric, - options, - customMetrics, -}: Props) => { - const [isPopoverOpen, setPopoverState] = useState(false); - const [mode, setMode] = useState('pick'); - const [editModeCustomMetrics, setEditModeCustomMetrics] = useState( - [] - ); - const [editCustomMetric, setEditCustomMetric] = useState(); - const handleClose = useCallback(() => { - setPopoverState(false); - }, [setPopoverState]); - - const handleToggle = useCallback(() => { - setPopoverState(!isPopoverOpen); - }, [isPopoverOpen]); - - const handleCustomMetric = useCallback( - (newMetric: SnapshotCustomMetricInput) => { - onChangeCustomMetrics([...customMetrics, newMetric]); - onChange(newMetric); - setMode('pick'); - }, - [customMetrics, onChange, onChangeCustomMetrics, setMode] - ); - - const setModeToEdit = useCallback(() => { - setMode('edit'); - setEditModeCustomMetrics(customMetrics); - }, [customMetrics]); - - const setModeToAdd = useCallback(() => { - setMode('addMetric'); - }, [setMode]); - - const setModeToPick = useCallback(() => { - setMode('pick'); - setEditModeCustomMetrics([]); - }, [setMode]); - - const handleDeleteCustomMetric = useCallback( - (m: SnapshotCustomMetricInput) => { - // If the metric we are deleting is the currently selected metric - // we need to change to the default. - if (SnapshotCustomMetricInputRT.is(metric) && m.id === metric.id) { - onChange({ type: options[0].value as SnapshotMetricType }); - } - // Filter out the deleted metric from the editbale. - const newMetrics = editModeCustomMetrics.filter(v => v.id !== m.id); - setEditModeCustomMetrics(newMetrics); - }, - [editModeCustomMetrics, metric, onChange, options] - ); - - const handleEditCustomMetric = useCallback( - (currentMetric: SnapshotCustomMetricInput) => { - const newMetrics = customMetrics.map(m => (m.id === currentMetric.id && currentMetric) || m); - onChangeCustomMetrics(newMetrics); - setModeToPick(); - setEditCustomMetric(void 0); - setEditModeCustomMetrics([]); - }, - [customMetrics, onChangeCustomMetrics, setModeToPick] - ); - - const handleSelectMetricToEdit = useCallback( - (currentMetric: SnapshotCustomMetricInput) => { - setEditCustomMetric(currentMetric); - setMode('editMetric'); - }, - [setMode, setEditCustomMetric] - ); - - const handleSaveEdit = useCallback(() => { - onChangeCustomMetrics(editModeCustomMetrics); - setMode('pick'); - }, [editModeCustomMetrics, onChangeCustomMetrics]); - - if (!options.length || !metric.type) { - throw Error( - i18n.translate('xpack.infra.waffle.unableToSelectMetricErrorTitle', { - defaultMessage: 'Unable to select options or value for metric.', - }) - ); - } - - const id = SnapshotCustomMetricInputRT.is(metric) && metric.id ? metric.id : metric.type; - const currentLabel = SnapshotCustomMetricInputRT.is(metric) - ? getCustomMetricLabel(metric) - : options.find(o => o.value === id)?.text; - - if (!currentLabel) { - return null; - } - - const button = ( - - - - ); - - return ( - - - {mode === 'pick' ? ( - - ) : null} - {mode === 'addMetric' ? ( - - ) : null} - {mode === 'editMetric' ? ( - - ) : null} - {mode === 'edit' ? ( - - ) : null} - - - - ); -}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_accounts_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_accounts_controls.tsx deleted file mode 100644 index 56c3a205b05d4..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/waffle_accounts_controls.tsx +++ /dev/null @@ -1,94 +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 { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { InventoryCloudAccount } from '../../../common/http_api/inventory_meta_api'; - -interface Props { - accountId: string; - options: InventoryCloudAccount[]; - changeAccount: (id: string) => void; -} - -export const WaffleAccountsControls = (props: Props) => { - const { accountId, options } = props; - const [isOpen, setIsOpen] = useState(); - - const showPopover = useCallback(() => { - setIsOpen(true); - }, [setIsOpen]); - - const closePopover = useCallback(() => { - setIsOpen(false); - }, [setIsOpen]); - - const currentLabel = options.find(o => o.value === accountId); - - const changeAccount = useCallback( - (val: string) => { - if (accountId === val) { - props.changeAccount(''); - } else { - props.changeAccount(val); - } - closePopover(); - }, - [accountId, closePopover, props] - ); - - const panels = useMemo( - () => [ - { - id: 0, - title: '', - items: options.map(o => { - const icon = o.value === accountId ? 'check' : 'empty'; - const panel = { name: o.name, onClick: () => changeAccount(o.value), icon }; - return panel; - }), - }, - ], - [options, accountId, changeAccount] - ); - - return ( - - - - - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - - - - ); -}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_region_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_region_controls.tsx deleted file mode 100644 index 52377bf4b8296..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/waffle_region_controls.tsx +++ /dev/null @@ -1,93 +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 { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -interface Props { - region?: string; - options: string[]; - changeRegion: (name: string) => void; -} - -export const WaffleRegionControls = (props: Props) => { - const { region, options } = props; - const [isOpen, setIsOpen] = useState(); - - const showPopover = useCallback(() => { - setIsOpen(true); - }, [setIsOpen]); - - const closePopover = useCallback(() => { - setIsOpen(false); - }, [setIsOpen]); - - const currentLabel = options.find(o => region === o); - - const changeRegion = useCallback( - (val: string) => { - if (region === val) { - props.changeRegion(''); - } else { - props.changeRegion(val); - } - closePopover(); - }, - [region, closePopover, props] - ); - - const panels = useMemo( - () => [ - { - id: 0, - title: '', - items: options.map(o => { - const icon = o === region ? 'check' : 'empty'; - const panel = { name: o, onClick: () => changeRegion(o), icon }; - return panel; - }), - }, - ], - [changeRegion, options, region] - ); - - return ( - - - - - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - - - - ); -}; diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx index d2fa49e8d5d9f..04f518aa9080f 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx +++ b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx @@ -17,7 +17,7 @@ import { MetricsExplorerYAxisMode, MetricsExplorerChartType, MetricsExplorerChartOptions, -} from './use_metrics_explorer_options'; +} from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; interface MetricsExplorerUrlState { timerange?: MetricsExplorerTimeOptions; diff --git a/x-pack/plugins/infra/public/containers/waffle/type_guards.ts b/x-pack/plugins/infra/public/containers/waffle/type_guards.ts deleted file mode 100644 index 3e21e3a56a6c6..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/type_guards.ts +++ /dev/null @@ -1,15 +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 { InfraWaffleMapGroupOfGroups, InfraWaffleMapGroupOfNodes } from '../../lib/lib'; - -export function isWaffleMapGroupWithNodes(subject: any): subject is InfraWaffleMapGroupOfNodes { - return subject && subject.nodes != null && Array.isArray(subject.nodes); -} - -export function isWaffleMapGroupWithGroups(subject: any): subject is InfraWaffleMapGroupOfGroups { - return subject && subject.groups != null && Array.isArray(subject.groups); -} diff --git a/x-pack/plugins/infra/public/containers/with_options.tsx b/x-pack/plugins/infra/public/containers/with_options.tsx deleted file mode 100644 index e18fc85a68d60..0000000000000 --- a/x-pack/plugins/infra/public/containers/with_options.tsx +++ /dev/null @@ -1,64 +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 moment from 'moment'; -import React from 'react'; - -import { euiPaletteColorBlind } from '@elastic/eui'; -import { InfraFormatterType, InfraOptions } from '../lib/lib'; -import { RendererFunction } from '../utils/typed_react'; - -const euiVisColorPalette = euiPaletteColorBlind(); - -const initialState = { - options: { - timerange: { - interval: '1m', - to: moment.utc().valueOf(), - from: moment - .utc() - .subtract(1, 'h') - .valueOf(), - }, - wafflemap: { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - metric: { type: 'cpu' }, - groupBy: [], - legend: { - type: 'gradient', - rules: [ - { - value: 0, - color: '#D3DAE6', - }, - { - value: 1, - color: euiVisColorPalette[1], - }, - ], - }, - }, - } as InfraOptions, -}; - -interface WithOptionsProps { - children: RendererFunction; -} - -type State = Readonly; - -export const withOptions = (WrappedComponent: React.ComponentType) => ( - {args => } -); - -export class WithOptions extends React.Component { - public readonly state: State = initialState; - - public render() { - return this.props.children(this.state.options); - } -} diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx index 13e054de2dcf7..f9cfaf71036f6 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx @@ -5,13 +5,14 @@ */ import { encode } from 'rison-node'; -import { createMemoryHistory, LocationDescriptorObject } from 'history'; +import { createMemoryHistory } from 'history'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { HistoryContext } from '../utils/history_context'; import { coreMock } from 'src/core/public/mocks'; import { useLinkProps, LinkDescriptor } from './use_link_props'; +import { ScopedHistory } from '../../../../../src/core/public'; const PREFIX = '/test-basepath/s/test-space/app/'; @@ -23,18 +24,13 @@ coreStartMock.application.getUrlForApp.mockImplementation((app, options) => { const INTERNAL_APP = 'metrics'; -// Note: Memory history doesn't support basename, -// we'll work around this by re-assigning 'createHref' so that -// it includes a basename, this then acts as our browserHistory instance would. const history = createMemoryHistory(); -const originalCreateHref = history.createHref; -history.createHref = (location: LocationDescriptorObject): string => { - return `${PREFIX}${INTERNAL_APP}${originalCreateHref.call(history, location)}`; -}; +history.push(`${PREFIX}${INTERNAL_APP}`); +const scopedHistory = new ScopedHistory(history, `${PREFIX}${INTERNAL_APP}`); const ProviderWrapper: React.FC = ({ children }) => { return ( - + {children}; ); @@ -111,7 +107,7 @@ describe('useLinkProps hook', () => { pathname: '/', }); expect(result.current.href).toBe('/test-basepath/s/test-space/app/ml/'); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with pathname options', () => { @@ -127,7 +123,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml/explorer?type=host&id=some-id&count=12345' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with hash options', () => { @@ -143,7 +139,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml#/explorer?type=host&id=some-id&count=12345' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with more complex encoding', () => { @@ -161,7 +157,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml#/explorer?type=host%20%2B%20host&name=this%20name%20has%20spaces%20and%20**%20and%20%25&id=some-id&count=12345&animals=dog,cat,bear' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with a consumer using Rison encoding for search', () => { @@ -180,7 +176,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/rison-app#rison-route?type=host%20%2B%20host&state=(refreshInterval:(pause:!t,value:0),time:(from:12345,to:54321))' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); }); }); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index e60ab32046832..8c522bb7fa764 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -9,7 +9,8 @@ import { stringify } from 'query-string'; import url from 'url'; import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public'; import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath'; -import { useHistory } from '../utils/history_context'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useNavigationWarningPrompt } from '../utils/navigation_warning_prompt'; type Search = Record; @@ -28,31 +29,26 @@ interface LinkProps { export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): LinkProps => { validateParams({ app, pathname, hash, search }); - const history = useHistory(); + const { prompt } = useNavigationWarningPrompt(); const prefixer = usePrefixPathWithBasepath(); + const navigateToApp = useKibana().services.application?.navigateToApp; const encodedSearch = useMemo(() => { return search ? encodeSearch(search) : undefined; }, [search]); - const internalLinkResult = useMemo(() => { - // When the logs / metrics apps are first mounted a history instance is setup with a 'basename' equal to the - // 'appBasePath' received from Core's 'AppMountParams', e.g. /BASE_PATH/s/SPACE_ID/app/APP_ID. With internal - // linking we are using 'createHref' and 'push' on top of this history instance. So a pathname of /inventory used within - // the metrics app will ultimatey end up as /BASE_PATH/s/SPACE_ID/app/metrics/inventory. React-router responds to this - // as it is instantiated with the same history instance. - return history?.createHref({ - pathname: pathname ? formatPathname(pathname) : undefined, - search: encodedSearch, - }); - }, [history, pathname, encodedSearch]); - - const externalLinkResult = useMemo(() => { + const mergedHash = useMemo(() => { // The URI spec defines that the query should appear before the fragment // https://tools.ietf.org/html/rfc3986#section-3 (e.g. url.format()). However, in Kibana, apps that use // hash based routing expect the query to be part of the hash. This will handle that. - const mergedHash = hash && encodedSearch ? `${hash}?${encodedSearch}` : hash; + return hash && encodedSearch ? `${hash}?${encodedSearch}` : hash; + }, [hash, encodedSearch]); + + const mergedPathname = useMemo(() => { + return pathname && encodedSearch ? `${pathname}?${encodedSearch}` : pathname; + }, [pathname, encodedSearch]); + const href = useMemo(() => { const link = url.format({ pathname, hash: mergedHash, @@ -60,28 +56,36 @@ export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): L }); return prefixer(app, link); - }, [hash, encodedSearch, pathname, prefixer, app]); + }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]); const onClick = useMemo(() => { - // If these results are equal we know we're trying to navigate within the same application - // that the current history instance is representing - if (internalLinkResult && linksAreEquivalent(externalLinkResult, internalLinkResult)) { - return (e: React.MouseEvent | React.MouseEvent) => { - e.preventDefault(); - if (history) { - history.push({ - pathname: pathname ? formatPathname(pathname) : undefined, - search: encodedSearch, - }); + return (e: React.MouseEvent | React.MouseEvent) => { + e.preventDefault(); + + const navigate = () => { + if (navigateToApp) { + const navigationPath = mergedHash ? `#${mergedHash}` : mergedPathname; + navigateToApp(app, { path: navigationPath ? navigationPath : undefined }); } }; - } else { - return undefined; - } - }, [internalLinkResult, externalLinkResult, history, pathname, encodedSearch]); + + // A component somewhere within the app hierarchy is requesting that we + // prompt the user before navigating. + if (prompt) { + const wantsToNavigate = window.confirm(prompt); + if (wantsToNavigate) { + navigate(); + } else { + return; + } + } else { + navigate(); + } + }; + }, [navigateToApp, mergedHash, mergedPathname, app, prompt]); return { - href: externalLinkResult, + href, onClick, }; }; @@ -90,10 +94,6 @@ const encodeSearch = (search: Search) => { return stringify(urlUtils.encodeQuery(search), { sort: false, encode: false }); }; -const formatPathname = (pathname: string) => { - return pathname[0] === '/' ? pathname : `/${pathname}`; -}; - const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { if (!app && hash) { throw new Error( @@ -101,9 +101,3 @@ const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { ); } }; - -const linksAreEquivalent = (externalLink: string, internalLink: string): boolean => { - // Compares with trailing slashes removed. This handles the case where the pathname is '/' - // and 'createHref' will include the '/' but Kibana's 'getUrlForApp' will remove it. - return externalLink.replace(/\/$/, '') === internalLink.replace(/\/$/, ''); -}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx deleted file mode 100644 index d592ae3480fc9..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx +++ /dev/null @@ -1,135 +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 { i18n } from '@kbn/i18n'; - -import React from 'react'; -import { Route, RouteComponentProps, Switch } from 'react-router-dom'; - -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { DocumentTitle } from '../../components/document_title'; -import { HelpCenterContent } from '../../components/help_center_content'; -import { RoutedTabs } from '../../components/navigation/routed_tabs'; -import { ColumnarPage } from '../../components/page'; -import { Header } from '../../components/header'; -import { MetricsExplorerOptionsContainer } from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; -import { WithSource } from '../../containers/with_source'; -import { Source } from '../../containers/source'; -import { MetricsExplorerPage } from './metrics_explorer'; -import { SnapshotPage } from './snapshot'; -import { MetricsSettingsPage } from './settings'; -import { AppNavigation } from '../../components/navigation/app_navigation'; -import { SourceLoadingPage } from '../../components/source_loading_page'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { WaffleOptionsProvider } from '../inventory_view/hooks/use_waffle_options'; -import { WaffleTimeProvider } from '../inventory_view/hooks/use_waffle_time'; -import { WaffleFiltersProvider } from '../inventory_view/hooks/use_waffle_filters'; -import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown'; - -export const InfrastructurePage = ({ match }: RouteComponentProps) => { - const uiCapabilities = useKibana().services.application?.capabilities; - - return ( - - - - - - - - - -
- - - - - - - - - - - - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - {configuration ? ( - - ) : ( - - )} - - )} - - )} - /> - - - - - - - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx deleted file mode 100644 index 0999cea59731c..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx +++ /dev/null @@ -1,100 +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 { i18n } from '@kbn/i18n'; - -import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; -import { DocumentTitle } from '../../../components/document_title'; -import { MetricsExplorerCharts } from '../../../components/metrics_explorer/charts'; -import { MetricsExplorerToolbar } from '../../../components/metrics_explorer/toolbar'; -import { SourceQuery } from '../../../../common/graphql/types'; -import { NoData } from '../../../components/empty_states'; -import { useMetricsExplorerState } from './use_metric_explorer_state'; -import { useTrackPageview } from '../../../../../observability/public'; - -interface MetricsExplorerPageProps { - source: SourceQuery.Query['source']['configuration']; - derivedIndexPattern: IIndexPattern; -} - -export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { - const { - loading, - error, - data, - currentTimerange, - options, - chartOptions, - setChartOptions, - handleAggregationChange, - handleMetricsChange, - handleFilterQuerySubmit, - handleGroupByChange, - handleTimeChange, - handleRefresh, - handleLoadMore, - defaultViewState, - onViewStateChange, - } = useMetricsExplorerState(source, derivedIndexPattern); - - useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' }); - useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); - - return ( - - - i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { - defaultMessage: '{previousTitle} | Metrics Explorer', - values: { - previousTitle, - }, - }) - } - /> - - {error ? ( - - ) : ( - - )} - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx deleted file mode 100644 index 48cc56388c0f2..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx +++ /dev/null @@ -1,105 +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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useContext } from 'react'; - -import { SnapshotToolbar } from './toolbar'; - -import { DocumentTitle } from '../../../components/document_title'; -import { NoIndices } from '../../../components/empty_states/no_indices'; -import { ColumnarPage } from '../../../components/page'; - -import { SourceErrorPage } from '../../../components/source_error_page'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; -import { Source } from '../../../containers/source'; -import { useTrackPageview } from '../../../../../observability/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { Layout } from '../../../components/inventory/layout'; -import { useLinkProps } from '../../../hooks/use_link_props'; - -export const SnapshotPage = () => { - const uiCapabilities = useKibana().services.application?.capabilities; - const { - hasFailedLoadingSource, - isLoading, - loadSourceFailureMessage, - loadSource, - metricIndicesExist, - } = useContext(Source.Context); - useTrackPageview({ app: 'infra_metrics', path: 'inventory' }); - useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 }); - - const tutorialLinkProps = useLinkProps({ - app: 'kibana', - hash: '/home/tutorial_directory/metrics', - }); - - return ( - - - i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { - defaultMessage: '{previousTitle} | Inventory', - values: { - previousTitle, - }, - }) - } - /> - {isLoading ? ( - - ) : metricIndicesExist ? ( - <> - - - - ) : hasFailedLoadingSource ? ( - - ) : ( - - - - {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', { - defaultMessage: 'View setup instructions', - })} - - - {uiCapabilities?.infrastructure?.configureSource ? ( - - - {i18n.translate('xpack.infra.configureSourceActionLabel', { - defaultMessage: 'Change source configuration', - })} - - - ) : null} - - } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx deleted file mode 100644 index ccdaa5e8dc785..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; - -import { Toolbar } from '../../../components/eui/toolbar'; -import { WaffleTimeControls } from '../../../components/waffle/waffle_time_controls'; -import { WaffleInventorySwitcher } from '../../../components/waffle/waffle_inventory_switcher'; -import { SearchBar } from '../../inventory_view/compontents/search_bar'; - -export const SnapshotToolbar = () => ( - - - - - - - - - - - - - -); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index b1dab3bd3f673..3ad242c77412d 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time'; +import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time'; import { useHostIpToName } from './use_host_ip_to_name'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { LoadingPage } from '../../components/loading_page'; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx index 72a41f5264244..3d25e4c6c258d 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time'; +import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { InventoryItemType } from '../../../common/inventory_models/types'; import { LinkDescriptor } from '../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/helpers.ts b/x-pack/plugins/infra/public/pages/metrics/components/helpers.ts deleted file mode 100644 index 4449196f2fb53..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/components/helpers.ts +++ /dev/null @@ -1,99 +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 { ReactText } from 'react'; -import Color from 'color'; -import { get, first, last, min, max } from 'lodash'; -import { createFormatter } from '../../../utils/formatters'; -import { InfraDataSeries } from '../../../graphql/types'; -import { - InventoryVisTypeRT, - InventoryFormatterType, - InventoryVisType, -} from '../../../../common/inventory_models/types'; -import { SeriesOverrides } from '../types'; -import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; - -/** - * Returns a formatter - */ -export const getFormatter = ( - formatter: InventoryFormatterType = 'number', - template: string = '{{value}}' -) => (val: ReactText) => (val != null ? createFormatter(formatter, template)(val) : ''); - -/** - * Does a series have more then two points? - */ -export const seriesHasLessThen2DataPoints = (series: InfraDataSeries): boolean => { - return series.data.length < 2; -}; - -/** - * Returns the minimum and maximum timestamp for a metric - */ -export const getMaxMinTimestamp = (metric: NodeDetailsMetricData): [number, number] => { - if (metric.series.some(seriesHasLessThen2DataPoints)) { - return [0, 0]; - } - const values = metric.series.reduce((acc, item) => { - const firstRow = first(item.data); - const lastRow = last(item.data); - return acc.concat([(firstRow && firstRow.timestamp) || 0, (lastRow && lastRow.timestamp) || 0]); - }, [] as number[]); - return [min(values), max(values)]; -}; - -/** - * Returns the chart name from the visConfig based on the series id, otherwise it - * just returns the seriesId - */ -export const getChartName = ( - seriesOverrides: SeriesOverrides | undefined, - seriesId: string, - label: string -) => { - if (!seriesOverrides) { - return label; - } - return get(seriesOverrides, [seriesId, 'name'], label); -}; - -/** - * Returns the chart color from the visConfig based on the series id, otherwise it - * just returns null if the color doesn't exists in the overrides. - */ -export const getChartColor = (seriesOverrides: SeriesOverrides | undefined, seriesId: string) => { - const rawColor: string | null = seriesOverrides - ? get(seriesOverrides, [seriesId, 'color']) - : null; - if (!rawColor) { - return null; - } - const color = new Color(rawColor); - return color.hex().toString(); -}; - -/** - * Gets the chart type based on the section and seriesId - */ -export const getChartType = ( - seriesOverrides: SeriesOverrides | undefined, - type: InventoryVisType | undefined, - seriesId: string -) => { - if (!seriesOverrides || !type) { - return 'line'; - } - const overrideValue = get(seriesOverrides, [seriesId, 'type']); - if (InventoryVisTypeRT.is(overrideValue)) { - return overrideValue; - } - if (InventoryVisTypeRT.is(type)) { - return type; - } - return 'line'; -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/components/series_chart.tsx deleted file mode 100644 index 849a5b8922165..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/components/series_chart.tsx +++ /dev/null @@ -1,87 +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 React from 'react'; -import { - AreaSeries, - BarSeries, - ScaleType, - RecursivePartial, - BarSeriesStyle, - AreaSeriesStyle, -} from '@elastic/charts'; -import { InfraDataSeries } from '../../../graphql/types'; -import { InventoryVisType } from '../../../../common/inventory_models/types'; - -interface Props { - id: string; - name: string; - color: string | null; - series: InfraDataSeries; - type: InventoryVisType; - stack: boolean | undefined; -} - -export const SeriesChart = (props: Props) => { - if (props.type === 'bar') { - return ; - } - return ; -}; - -export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { - const style: RecursivePartial = { - area: { - opacity: 1, - visible: 'area' === type, - }, - line: { - strokeWidth: 'area' === type ? 1 : 2, - visible: true, - }, - }; - return ( - - ); -}; - -export const BarChart = ({ id, color, series, name, type, stack }: Props) => { - const style: RecursivePartial = { - rectBorder: { - stroke: color || void 0, - strokeWidth: 1, - visible: true, - }, - rect: { - opacity: 1, - }, - }; - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 531be40d2dc43..cc88dd9e0d0f8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -3,138 +3,133 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { i18n } from '@kbn/i18n'; -import React, { useContext, useState } from 'react'; -import { euiStyled, EuiTheme, withTheme } from '../../../../observability/public'; + +import React from 'react'; +import { Route, RouteComponentProps, Switch } from 'react-router-dom'; + +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { DocumentTitle } from '../../components/document_title'; +import { HelpCenterContent } from '../../components/help_center_content'; +import { RoutedTabs } from '../../components/navigation/routed_tabs'; +import { ColumnarPage } from '../../components/page'; import { Header } from '../../components/header'; -import { ColumnarPage, PageContent } from '../../components/page'; -import { withMetricPageProviders } from './page_providers'; -import { useMetadata } from '../../containers/metadata/use_metadata'; +import { MetricsExplorerOptionsContainer } from './metrics_explorer/hooks/use_metrics_explorer_options'; +import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; +import { WithSource } from '../../containers/with_source'; import { Source } from '../../containers/source'; -import { InfraLoadingPanel } from '../../components/loading'; -import { findInventoryModel } from '../../../common/inventory_models'; -import { NavItem } from './lib/side_nav_context'; -import { NodeDetailsPage } from './components/node_details_page'; +import { MetricsExplorerPage } from './metrics_explorer'; +import { SnapshotPage } from './inventory_view'; +import { MetricsSettingsPage } from './settings'; +import { AppNavigation } from '../../components/navigation/app_navigation'; +import { SourceLoadingPage } from '../../components/source_loading_page'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { useMetricsTimeContext } from './hooks/use_metrics_time'; -import { useLinkProps } from '../../hooks/use_link_props'; - -const DetailPageContent = euiStyled(PageContent)` - overflow: auto; - background-color: ${props => props.theme.eui.euiColorLightestShade}; -`; - -interface Props { - theme: EuiTheme; - match: { - params: { - type: string; - node: string; - }; - }; -} - -export const MetricDetail = withMetricPageProviders( - withTheme(({ match }: Props) => { - const uiCapabilities = useKibana().services.application?.capabilities; - const nodeId = match.params.node; - const nodeType = match.params.type as InventoryItemType; - const inventoryModel = findInventoryModel(nodeType); - const { sourceId } = useContext(Source.Context); - const { - timeRange, - parsedTimeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - triggerRefresh, - } = useMetricsTimeContext(); - const { - name, - filteredRequiredMetrics, - loading: metadataLoading, - cloudId, - metadata, - } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); - - const [sideNav, setSideNav] = useState([]); +import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options'; +import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; +import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; +import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown'; - const addNavItem = React.useCallback( - (item: NavItem) => { - if (!sideNav.some(n => n.id === item.id)) { - setSideNav([item, ...sideNav]); - } - }, - [sideNav] - ); +export const InfrastructurePage = ({ match }: RouteComponentProps) => { + const uiCapabilities = useKibana().services.application?.capabilities; - const metricsLinkProps = useLinkProps({ - app: 'metrics', - pathname: '/', - }); + return ( + + + + + + - const breadcrumbs = [ - { - ...metricsLinkProps, - text: i18n.translate('xpack.infra.header.infrastructureTitle', { - defaultMessage: 'Metrics', - }), - }, - { text: name }, - ]; + - if (metadataLoading && !filteredRequiredMetrics.length) { - return ( - - ); - } +
+ + + + + + + + + + - return ( - -
- - - {metadata ? ( - - ) : null} - - - ); - }) -); + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + {configuration ? ( + + ) : ( + + )} + + )} + + )} + /> + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx new file mode 100644 index 0000000000000..f0bc404dc3797 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx @@ -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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import React, { ReactNode } from 'react'; +import { withTheme, EuiTheme } from '../../../../../../observability/public'; + +interface Props { + label: string; + onClick: () => void; + theme: EuiTheme; + children: ReactNode; +} + +export const DropdownButton = withTheme(({ onClick, label, theme, children }: Props) => { + return ( + + + {label} + + + + {children} + + + + ); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx new file mode 100644 index 0000000000000..708d5f7d75907 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx @@ -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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import { WaffleTimeControls } from './waffle/waffle_time_controls'; +import { SearchBar } from './search_bar'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; + +export const FilterBar = () => ( + + + + + + + + + + +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx new file mode 100644 index 0000000000000..a71e43874b480 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -0,0 +1,151 @@ +/* + * 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, { useCallback } from 'react'; +import { useInterval } from 'react-use'; + +import { euiPaletteColorBlind, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; +import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview'; +import { PageContent } from '../../../../components/page'; +import { useSnapshot } from '../hooks/use_snaphot'; +import { useWaffleTimeContext } from '../hooks/use_waffle_time'; +import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; +import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; +import { useSourceContext } from '../../../../containers/source'; +import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../../../lib/lib'; +import { euiStyled } from '../../../../../../observability/public'; +import { Toolbar } from './toolbars/toolbar'; +import { ViewSwitcher } from './waffle/view_switcher'; +import { SavedViews } from './saved_views'; +import { IntervalLabel } from './waffle/interval_label'; +import { Legend } from './waffle/legend'; +import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; + +const euiVisColorPalette = euiPaletteColorBlind(); + +export const Layout = () => { + const { sourceId, source } = useSourceContext(); + const { + metric, + groupBy, + nodeType, + accountId, + region, + changeView, + view, + autoBounds, + boundsOverride, + } = useWaffleOptionsContext(); + const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); + const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); + const { loading, nodes, reload, interval } = useSnapshot( + filterQueryAsJson, + metric, + groupBy, + nodeType, + sourceId, + currentTime, + accountId, + region + ); + + const options = { + formatter: InfraFormatterType.percent, + formatTemplate: '{{value}}', + legend: { + type: 'gradient', + rules: [ + { value: 0, color: '#D3DAE6' }, + { value: 1, color: euiVisColorPalette[1] }, + ], + } as InfraWaffleMapGradientLegend, + metric, + fields: source?.configuration?.fields, + groupBy, + }; + + useInterval( + () => { + if (!loading) { + jumpToTime(Date.now()); + } + }, + isAutoReloading ? 5000 : null + ); + + const intervalAsString = convertIntervalToString(interval); + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; + const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const MainContainer = euiStyled.div` + position: relative; + flex: 1 1 auto; +`; + +const TopActionContainer = euiStyled.div` + padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`}; +`; + +const BottomActionContainer = euiStyled.div` + background-color: ${props => props.theme.eui.euiPageBackgroundColor}; + padding: ${props => props.theme.eui.paddingSizes.m} ${props => + props.theme.eui.paddingSizes.m} ${props => props.theme.eui.paddingSizes.s}; + position: absolute; + left: 0; + bottom: 4px; + right: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx new file mode 100644 index 0000000000000..966a327f40bc1 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -0,0 +1,149 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { max, min } from 'lodash'; +import React, { useCallback } from 'react'; + +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { euiStyled } from '../../../../../../observability/public'; +import { InfraWaffleMapBounds, InfraWaffleMapOptions, InfraFormatter } from '../../../../lib/lib'; +import { NoData } from '../../../../components/empty_states'; +import { InfraLoadingPanel } from '../../../../components/loading'; +import { Map } from './waffle/map'; +import { TableView } from './table_view'; +import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; + +export interface KueryFilterQuery { + kind: 'kuery'; + expression: string; +} + +interface Props { + options: InfraWaffleMapOptions; + nodeType: InventoryItemType; + nodes: SnapshotNode[]; + loading: boolean; + reload: () => void; + onDrilldown: (filter: KueryFilterQuery) => void; + currentTime: number; + view: string; + boundsOverride: InfraWaffleMapBounds; + autoBounds: boolean; + formatter: InfraFormatter; +} + +export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { + const maxValues = nodes.map(node => node.metric.max); + const minValues = nodes.map(node => node.metric.value); + // if there is only one value then we need to set the bottom range to zero for min + // otherwise the legend will look silly since both values are the same for top and + // bottom. + if (minValues.length === 1) { + minValues.unshift(0); + } + return { min: min(minValues) || 0, max: max(maxValues) || 0 }; +}; + +export const NodesOverview = ({ + autoBounds, + boundsOverride, + loading, + nodes, + nodeType, + reload, + view, + currentTime, + options, + formatter, + onDrilldown, +}: Props) => { + const handleDrilldown = useCallback( + (filter: string) => { + onDrilldown({ + kind: 'kuery', + expression: filter, + }); + return; + }, + [onDrilldown] + ); + + const noData = !loading && nodes && nodes.length === 0; + if (loading) { + return ( + + ); + } else if (noData) { + return ( + { + reload(); + }} + testString="noMetricsDataPrompt" + /> + ); + } + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; + + if (view === 'table') { + return ( + + + + ); + } + return ( + + + + ); +}; + +const TableContainer = euiStyled.div` + padding: ${props => props.theme.eui.paddingSizes.l}; +`; + +const MapContainer = euiStyled.div` + position: absolute; + display: flex; + top: 70px; + right: 0; + bottom: 0; + left: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx new file mode 100644 index 0000000000000..356f0598e00d2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.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 from 'react'; +import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { inventoryViewSavedObjectType } from '../../../../../common/saved_objects/inventory_view'; +import { useWaffleViewState } from '../hooks/use_waffle_view_state'; + +export const SavedViews = () => { + const { viewState, defaultViewState, onViewChange } = useWaffleViewState(); + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx similarity index 86% rename from x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx index f4fde46d434f8..1f84ef3685f34 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx @@ -5,9 +5,9 @@ */ import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { Source } from '../../../containers/source'; -import { AutocompleteField } from '../../../components/autocomplete_field'; -import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion'; +import { Source } from '../../../../containers/source'; +import { AutocompleteField } from '../../../../components/autocomplete_field'; +import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; export const SearchBar = () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx new file mode 100644 index 0000000000000..0557343e735f9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx @@ -0,0 +1,170 @@ +/* + * 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 { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { last } from 'lodash'; +import React, { useState, useCallback, useEffect } from 'react'; +import { createWaffleMapNode } from '../lib/nodes_to_wafflemap'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; +import { fieldToName } from '../lib/field_to_display_name'; +import { NodeContextMenu } from './waffle/node_context_menu'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { SnapshotNode, SnapshotNodePath } from '../../../../../common/http_api/snapshot_api'; +import { CONTAINER_CLASSNAME } from '../../../../apps/start_app'; + +interface Props { + nodes: SnapshotNode[]; + nodeType: InventoryItemType; + options: InfraWaffleMapOptions; + currentTime: number; + formatter: (subject: string | number) => string; + onFilter: (filter: string) => void; +} + +const getGroupPaths = (path: SnapshotNodePath[]) => { + switch (path.length) { + case 3: + return path.slice(0, 2); + case 2: + return path.slice(0, 1); + default: + return []; + } +}; + +export const TableView = (props: Props) => { + const { nodes, options, formatter, currentTime, nodeType } = props; + const [openPopovers, setOpenPopovers] = useState([]); + const openPopoverFor = useCallback( + (id: string) => () => { + setOpenPopovers([...openPopovers, id]); + }, + [openPopovers] + ); + + const closePopoverFor = useCallback( + (id: string) => () => { + if (openPopovers.includes(id)) { + setOpenPopovers(openPopovers.filter(subject => subject !== id)); + } + }, + [openPopovers] + ); + + useEffect(() => { + const el = document.getElementsByClassName(CONTAINER_CLASSNAME)[0]; + if (el instanceof HTMLElement) { + if (openPopovers.length > 0) { + el.style.overflowY = 'hidden'; + } else { + el.style.overflowY = 'auto'; + } + } + }, [openPopovers]); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }), + sortable: true, + truncateText: true, + textOnly: true, + render: (value: string, item: { node: InfraWaffleMapNode }) => { + const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`; + // For the table we need to create a UniqueID that takes into to account the groupings + // as well as the node name. There is the possibility that a node can be present in two + // different groups and be on the screen at the same time. + const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':'); + return ( + + + {value} + + + ); + }, + }, + ...options.groupBy.map((grouping, index) => ({ + field: `group_${index}`, + name: fieldToName((grouping && grouping.field) || ''), + sortable: true, + truncateText: true, + textOnly: true, + render: (value: string) => { + const handleClick = () => props.onFilter(`${grouping.field}:"${value}"`); + return ( + + {value} + + ); + }, + })), + { + field: 'value', + name: i18n.translate('xpack.infra.tableView.columnName.last1m', { + defaultMessage: 'Last 1m', + }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + { + field: 'avg', + name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + { + field: 'max', + name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + ]; + + const items = nodes.map(node => { + const name = last(node.path); + return { + name: (name && name.label) || 'unknown', + ...getGroupPaths(node.path).reduce( + (acc, path, index) => ({ + ...acc, + [`group_${index}`]: path.label, + }), + {} + ), + value: node.metric.value, + avg: node.metric.avg, + max: node.metric.max, + node: createWaffleMapNode(node), + }; + }); + const initialSorting = { + sort: { + field: 'value', + direction: 'desc', + }, + } as const; + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx new file mode 100644 index 0000000000000..e8485fb812586 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -0,0 +1,64 @@ +/* + * 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'; +import { EuiFlexItem } from '@elastic/eui'; +import { useSourceContext } from '../../../../../containers/source'; +import { + SnapshotMetricInput, + SnapshotGroupBy, + SnapshotCustomMetricInput, +} from '../../../../../../common/http_api/snapshot_api'; +import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api'; +import { findToolbar } from '../../../../../../common/inventory_models/toolbars'; +import { ToolbarWrapper } from './toolbar_wrapper'; + +import { InfraGroupByOptions } from '../../../../../lib/lib'; +import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { WaffleOptionsState } from '../../hooks/use_waffle_options'; +import { useInventoryMeta } from '../../hooks/use_inventory_meta'; + +export interface ToolbarProps + extends Omit { + createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + changeMetric: (payload: SnapshotMetricInput) => void; + changeGroupBy: (payload: SnapshotGroupBy) => void; + changeCustomOptions: (payload: InfraGroupByOptions[]) => void; + changeAccount: (id: string) => void; + changeRegion: (name: string) => void; + accounts: InventoryCloudAccount[]; + regions: string[]; + changeCustomMetrics: (payload: SnapshotCustomMetricInput[]) => void; +} + +const wrapToolbarItems = ( + ToolbarItems: FunctionComponent, + accounts: InventoryCloudAccount[], + regions: string[] +) => { + return ( + + {props => ( + <> + + + + )} + + ); +}; + +interface Props { + nodeType: InventoryItemType; +} + +export const Toolbar = ({ nodeType }: Props) => { + const { sourceId } = useSourceContext(); + const { accounts, regions } = useInventoryMeta(sourceId, nodeType); + const ToolbarItems = findToolbar(nodeType); + return wrapToolbarItems(ToolbarItems, accounts, regions); +}; diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx similarity index 88% rename from x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index fefda94372cfb..ea53122984161 100644 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -5,14 +5,14 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { Toolbar } from '../../eui/toolbar'; +import { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; +import { fieldToName } from '../../lib/field_to_display_name'; +import { useSourceContext } from '../../../../../containers/source'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher'; import { ToolbarProps } from './toolbar'; -import { fieldToName } from '../../waffle/lib/field_to_display_name'; -import { useSourceContext } from '../../../containers/source'; -import { useWaffleOptionsContext } from '../../../pages/inventory_view/hooks/use_waffle_options'; interface Props { children: (props: Omit) => React.ReactElement; @@ -36,26 +36,27 @@ export const ToolbarWrapper = (props: Props) => { } = useWaffleOptionsContext(); const { createDerivedIndexPattern } = useSourceContext(); return ( - - - {props.children({ - createDerivedIndexPattern, - changeMetric, - changeGroupBy, - changeAccount, - changeRegion, - changeCustomOptions, - customOptions, - groupBy, - metric, - nodeType, - region, - accountId, - customMetrics, - changeCustomMetrics, - })} - - + <> + + + + {props.children({ + createDerivedIndexPattern, + changeMetric, + changeGroupBy, + changeAccount, + changeRegion, + changeCustomOptions, + customOptions, + groupBy, + metric, + nodeType, + region, + accountId, + customMetrics, + changeCustomMetrics, + })} + ); }; diff --git a/x-pack/plugins/infra/public/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/conditional_tooltip.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx diff --git a/x-pack/plugins/infra/public/components/waffle/custom_field_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx similarity index 97% rename from x-pack/plugins/infra/public/components/waffle/custom_field_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx index d2dc535f6d6b3..090d53f1ff737 100644 --- a/x-pack/plugins/infra/public/components/waffle/custom_field_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { InfraGroupByOptions } from '../../lib/lib'; +import { InfraGroupByOptions } from '../../../../../lib/lib'; interface Props { onSubmit: (field: string) => void; diff --git a/x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx index 6b0c4bb41dc98..87f7e4cbff11e 100644 --- a/x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx @@ -6,13 +6,13 @@ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapGradientLegend, InfraWaffleMapGradientRule, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; interface Props { legend: InfraWaffleMapGradientLegend; diff --git a/x-pack/plugins/infra/public/components/waffle/group_name.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/group_name.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx index 01bd3600a1624..308460203b132 100644 --- a/x-pack/plugins/infra/public/components/waffle/group_name.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx @@ -6,8 +6,8 @@ import { EuiLink, EuiToolTip } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; -import { InfraWaffleMapGroup, InfraWaffleMapOptions } from '../../lib/lib'; +import { euiStyled } from '../../../../../../../observability/public'; +import { InfraWaffleMapGroup, InfraWaffleMapOptions } from '../../../../../lib/lib'; interface Props { onDrilldown: (filter: string) => void; diff --git a/x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx similarity index 90% rename from x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx index 9634293587d49..6b3f22007f580 100644 --- a/x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx @@ -6,15 +6,15 @@ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfGroups, InfraWaffleMapOptions, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; import { GroupName } from './group_name'; import { GroupOfNodes } from './group_of_nodes'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; interface Props { onDrilldown: (filter: string) => void; diff --git a/x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx similarity index 90% rename from x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx index 6b82671617df7..fc438ed4ca0a2 100644 --- a/x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx @@ -6,15 +6,15 @@ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfNodes, InfraWaffleMapOptions, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; import { GroupName } from './group_name'; import { Node } from './node'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; interface Props { onDrilldown: (filter: string) => void; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx new file mode 100644 index 0000000000000..dbbfb0f49c0e9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + intervalAsString?: string; +} + +export const IntervalLabel = ({ intervalAsString }: Props) => { + if (!intervalAsString) { + return null; + } + + return ( + +

+ +

+
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx similarity index 88% rename from x-pack/plugins/infra/public/components/waffle/legend.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx index 13e533b225d4d..ac699f96a75a6 100644 --- a/x-pack/plugins/infra/public/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -5,13 +5,13 @@ */ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; -import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../lib/lib'; +import { euiStyled } from '../../../../../../../observability/public'; +import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../../../../lib/lib'; import { GradientLegend } from './gradient_legend'; import { LegendControls } from './legend_controls'; -import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './lib/type_guards'; +import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from '../../lib/type_guards'; import { StepLegend } from './steps_legend'; -import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; interface Props { legend: InfraWaffleMapLegend; bounds: InfraWaffleMapBounds; @@ -53,7 +53,7 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter const LegendContainer = euiStyled.div` position: absolute; - bottom: 10px; + bottom: 0px; left: 10px; right: 10px; `; diff --git a/x-pack/plugins/infra/public/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/legend_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 26b5b1c0af727..30447e5244241 100644 --- a/x-pack/plugins/infra/public/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -23,8 +23,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { SyntheticEvent, useState } from 'react'; -import { euiStyled } from '../../../../observability/public'; -import { InfraWaffleMapBounds } from '../../lib/lib'; +import { euiStyled } from '../../../../../../../observability/public'; +import { InfraWaffleMapBounds } from '../../../../../lib/lib'; interface Props { onChange: (options: { auto: boolean; bounds: InfraWaffleMapBounds }) => void; @@ -40,7 +40,7 @@ export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBound const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop const buttonComponent = ( = ({ } })} - ); }} diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/custom_metric_form.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx similarity index 97% rename from x-pack/plugins/infra/public/components/waffle/metric_control/custom_metric_form.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx index 26e42061ed10b..aec624c0aff1f 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/custom_metric_form.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx @@ -26,8 +26,11 @@ import { SnapshotCustomMetricInput, SNAPSHOT_CUSTOM_AGGREGATIONS, SnapshotCustomAggregationRT, -} from '../../../../common/http_api/snapshot_api'; -import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components'; +} from '../../../../../../../common/http_api/snapshot_api'; +import { + EuiTheme, + withTheme, +} from '../../../../../../../../../legacy/common/eui_styled_components'; interface SelectedOption { label: string; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/get_custom_metric_label.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts similarity index 91% rename from x-pack/plugins/infra/public/components/waffle/metric_control/get_custom_metric_label.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts index 4f88c1b29c1f2..495cc8197d2e7 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/get_custom_metric_label.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; +import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; export const getCustomMetricLabel = (metric: SnapshotCustomMetricInput) => { const METRIC_LABELS = { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx new file mode 100644 index 0000000000000..f91e9a4034bc2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx @@ -0,0 +1,195 @@ +/* + * 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 { EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback } from 'react'; +import { IFieldType } from 'src/plugins/data/public'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInput, + SnapshotCustomMetricInputRT, +} from '../../../../../../../common/http_api/snapshot_api'; +import { CustomMetricForm } from './custom_metric_form'; +import { getCustomMetricLabel } from './get_custom_metric_label'; +import { MetricsContextMenu } from './metrics_context_menu'; +import { ModeSwitcher } from './mode_switcher'; +import { MetricsEditMode } from './metrics_edit_mode'; +import { CustomMetricMode } from './types'; +import { SnapshotMetricType } from '../../../../../../../common/inventory_models/types'; +import { DropdownButton } from '../../dropdown_button'; + +interface Props { + options: Array<{ text: string; value: string }>; + metric: SnapshotMetricInput; + fields: IFieldType[]; + onChange: (metric: SnapshotMetricInput) => void; + onChangeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void; + customMetrics: SnapshotCustomMetricInput[]; +} + +export const WaffleMetricControls = ({ + fields, + onChange, + onChangeCustomMetrics, + metric, + options, + customMetrics, +}: Props) => { + const [isPopoverOpen, setPopoverState] = useState(false); + const [mode, setMode] = useState('pick'); + const [editModeCustomMetrics, setEditModeCustomMetrics] = useState( + [] + ); + const [editCustomMetric, setEditCustomMetric] = useState(); + const handleClose = useCallback(() => { + setPopoverState(false); + }, [setPopoverState]); + + const handleToggle = useCallback(() => { + setPopoverState(!isPopoverOpen); + }, [isPopoverOpen]); + + const handleCustomMetric = useCallback( + (newMetric: SnapshotCustomMetricInput) => { + onChangeCustomMetrics([...customMetrics, newMetric]); + onChange(newMetric); + setMode('pick'); + }, + [customMetrics, onChange, onChangeCustomMetrics, setMode] + ); + + const setModeToEdit = useCallback(() => { + setMode('edit'); + setEditModeCustomMetrics(customMetrics); + }, [customMetrics]); + + const setModeToAdd = useCallback(() => { + setMode('addMetric'); + }, [setMode]); + + const setModeToPick = useCallback(() => { + setMode('pick'); + setEditModeCustomMetrics([]); + }, [setMode]); + + const handleDeleteCustomMetric = useCallback( + (m: SnapshotCustomMetricInput) => { + // If the metric we are deleting is the currently selected metric + // we need to change to the default. + if (SnapshotCustomMetricInputRT.is(metric) && m.id === metric.id) { + onChange({ type: options[0].value as SnapshotMetricType }); + } + // Filter out the deleted metric from the editbale. + const newMetrics = editModeCustomMetrics.filter(v => v.id !== m.id); + setEditModeCustomMetrics(newMetrics); + }, + [editModeCustomMetrics, metric, onChange, options] + ); + + const handleEditCustomMetric = useCallback( + (currentMetric: SnapshotCustomMetricInput) => { + const newMetrics = customMetrics.map(m => (m.id === currentMetric.id && currentMetric) || m); + onChangeCustomMetrics(newMetrics); + setModeToPick(); + setEditCustomMetric(void 0); + setEditModeCustomMetrics([]); + }, + [customMetrics, onChangeCustomMetrics, setModeToPick] + ); + + const handleSelectMetricToEdit = useCallback( + (currentMetric: SnapshotCustomMetricInput) => { + setEditCustomMetric(currentMetric); + setMode('editMetric'); + }, + [setMode, setEditCustomMetric] + ); + + const handleSaveEdit = useCallback(() => { + onChangeCustomMetrics(editModeCustomMetrics); + setMode('pick'); + }, [editModeCustomMetrics, onChangeCustomMetrics]); + + if (!options.length || !metric.type) { + throw Error( + i18n.translate('xpack.infra.waffle.unableToSelectMetricErrorTitle', { + defaultMessage: 'Unable to select options or value for metric.', + }) + ); + } + + const id = SnapshotCustomMetricInputRT.is(metric) && metric.id ? metric.id : metric.type; + const currentLabel = SnapshotCustomMetricInputRT.is(metric) + ? getCustomMetricLabel(metric) + : options.find(o => o.value === id)?.text; + + if (!currentLabel) { + return null; + } + + const button = ( + + {currentLabel} + + ); + + return ( + <> + + {mode === 'pick' ? ( + + ) : null} + {mode === 'addMetric' ? ( + + ) : null} + {mode === 'editMetric' ? ( + + ) : null} + {mode === 'edit' ? ( + + ) : null} + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx similarity index 94% rename from x-pack/plugins/infra/public/components/waffle/metric_control/metrics_context_menu.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx index 1aacf54244c37..1cdef493aee4f 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx @@ -9,11 +9,11 @@ import { SnapshotMetricInput, SnapshotCustomMetricInput, SnapshotCustomMetricInputRT, -} from '../../../../common/http_api/snapshot_api'; +} from '../../../../../../../common/http_api/snapshot_api'; import { SnapshotMetricTypeRT, SnapshotMetricType, -} from '../../../../common/inventory_models/types'; +} from '../../../../../../../common/inventory_models/types'; import { getCustomMetricLabel } from './get_custom_metric_label'; interface Props { diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_edit_mode.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/waffle/metric_control/metrics_edit_mode.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx index ba1f46815db20..4d1bc906de0b9 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_edit_mode.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx @@ -6,9 +6,12 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; +import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; import { getCustomMetricLabel } from './get_custom_metric_label'; -import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components'; +import { + EuiTheme, + withTheme, +} from '../../../../../../../../../legacy/common/eui_styled_components'; interface Props { theme: EuiTheme; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/mode_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx similarity index 95% rename from x-pack/plugins/infra/public/components/waffle/metric_control/mode_switcher.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx index 43bb904594c68..acb740f1750c8 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/mode_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx @@ -8,8 +8,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CustomMetricMode } from './types'; -import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; -import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components'; +import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; +import { + EuiTheme, + withTheme, +} from '../../../../../../../../../legacy/common/eui_styled_components'; interface Props { theme: EuiTheme; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/types.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/types.ts similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/metric_control/types.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/types.ts diff --git a/x-pack/plugins/infra/public/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx similarity index 93% rename from x-pack/plugins/infra/public/components/waffle/node.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index 4eb5ccb8c1b4d..fd5b5b01f329f 100644 --- a/x-pack/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -10,11 +10,15 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ConditionalToolTip } from './conditional_tooltip'; -import { euiStyled } from '../../../../observability/public'; -import { InfraWaffleMapBounds, InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; -import { colorFromValue } from './lib/color_from_value'; +import { euiStyled } from '../../../../../../../observability/public'; +import { + InfraWaffleMapBounds, + InfraWaffleMapNode, + InfraWaffleMapOptions, +} from '../../../../../lib/lib'; +import { colorFromValue } from '../../lib/color_from_value'; import { NodeContextMenu } from './node_context_menu'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; const initialState = { isPopoverOpen: false, diff --git a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx similarity index 91% rename from x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 5f05cebd8f616..275635a33ec26 100644 --- a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -9,12 +9,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo, useState } from 'react'; -import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; -import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to'; -import { createUptimeLink } from './lib/create_uptime_link'; -import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { AlertFlyout } from '../../../../../components/alerting/metrics/alert_flyout'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; +import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to'; +import { createUptimeLink } from '../../lib/create_uptime_link'; +import { findInventoryModel, findInventoryFields } from '../../../../../../common/inventory_models'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { Section, SectionLinkProps, @@ -23,9 +24,8 @@ import { SectionSubtitle, SectionLinks, SectionLink, -} from '../../../../observability/public'; -import { useLinkProps } from '../../hooks/use_link_props'; -import { AlertFlyout } from '../alerting/metrics/alert_flyout'; +} from '../../../../../../../observability/public'; +import { useLinkProps } from '../../../../../hooks/use_link_props'; interface Props { options: InfraWaffleMapOptions; diff --git a/x-pack/plugins/infra/public/components/waffle/steps_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx similarity index 95% rename from x-pack/plugins/infra/public/components/waffle/steps_legend.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx index d2079d53dad71..1ef0f2d0c4288 100644 --- a/x-pack/plugins/infra/public/components/waffle/steps_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx @@ -7,13 +7,13 @@ import { darken } from 'polished'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraFormatter, InfraWaffleMapRuleOperator, InfraWaffleMapStepLegend, InfraWaffleMapStepRule, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; const OPERATORS = { [InfraWaffleMapRuleOperator.gte]: '>=', diff --git a/x-pack/plugins/infra/public/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/waffle/view_switcher.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx index 78a2cad9ca7ee..76756637eb69e 100644 --- a/x-pack/plugins/infra/public/components/waffle/view_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx @@ -28,7 +28,7 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { label: i18n.translate('xpack.infra.viewSwitcher.tableViewLabel', { defaultMessage: 'Table view', }), - iconType: 'editorUnorderedList', + iconType: 'visTable', }, ]; return ( @@ -37,9 +37,11 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { defaultMessage: 'Switch between table and map view', })} options={buttons} - color="primary" + color="text" + buttonSize="m" idSelected={view} onChange={onChange} + isIconOnly /> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx new file mode 100644 index 0000000000000..3e4ff1de8291d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api'; +import { DropdownButton } from '../dropdown_button'; + +interface Props { + accountId: string; + options: InventoryCloudAccount[]; + changeAccount: (id: string) => void; +} + +export const WaffleAccountsControls = (props: Props) => { + const { accountId, options } = props; + const [isOpen, setIsOpen] = useState(false); + + const showPopover = useCallback(() => { + setIsOpen(true); + }, [setIsOpen]); + + const closePopover = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const currentLabel = options.find(o => o.value === accountId); + + const changeAccount = useCallback( + (val: string) => { + if (accountId === val) { + props.changeAccount(''); + } else { + props.changeAccount(val); + } + closePopover(); + }, + [accountId, closePopover, props] + ); + + const panels = useMemo( + () => [ + { + id: 0, + title: '', + items: options.map(o => { + const icon = o.value === accountId ? 'check' : 'empty'; + const panel = { name: o.name, onClick: () => changeAccount(o.value), icon }; + return panel; + }), + }, + ], + [options, accountId, changeAccount] + ); + + const button = ( + + {currentLabel + ? currentLabel.name + : i18n.translate('xpack.infra.waffle.accountAllTitle', { + defaultMessage: 'All', + })} + + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx similarity index 85% rename from x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 1a3cef591bc07..c1f406f31e85e 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -9,19 +9,18 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor, - EuiFilterButton, - EuiFilterGroup, EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { InfraGroupByOptions } from '../../lib/lib'; +import { InfraGroupByOptions } from '../../../../../lib/lib'; import { CustomFieldPanel } from './custom_field_panel'; -import { euiStyled } from '../../../../observability/public'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { SnapshotGroupBy } from '../../../common/http_api/snapshot_api'; +import { euiStyled } from '../../../../../../../observability/public'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api'; +import { DropdownButton } from '../dropdown_button'; interface Props { options: Array<{ text: string; field: string; toolTipContent?: string }>; @@ -121,29 +120,31 @@ export const WaffleGroupByControls = class extends React.PureComponent o != null) // In this map the `o && o.field` is totally unnecessary but Typescript is // too stupid to realize that the filter above prevents the next map from being null - .map(o => {o && o.text}) + .map(o => ( + + {o && o.text} + + )) ) : ( ); + const button = ( - - + {buttonBody} - + ); return ( - - - - - + + + ); } diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx similarity index 75% rename from x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx index 21da10a0a7650..e534c97eda090 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx @@ -4,19 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiPopover, - EuiContextMenu, - EuiFilterButton, - EuiFilterGroup, - EuiContextMenuPanelDescriptor, -} from '@elastic/eui'; +import { EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { findInventoryModel } from '../../../common/inventory_models'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; +import { findInventoryModel } from '../../../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { DropdownButton } from '../dropdown_button'; const getDisplayNameForType = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); @@ -120,27 +114,23 @@ export const WaffleInventorySwitcher: React.FC = () => { return getDisplayNameForType(nodeType); }, [nodeType]); + const button = ( + + {selectedText} + + ); + return ( - - - - - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" - withTitle - anchorPosition="downLeft" - > - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx new file mode 100644 index 0000000000000..9d759424cdc93 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx @@ -0,0 +1,80 @@ +/* + * 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 { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { DropdownButton } from '../dropdown_button'; + +interface Props { + region?: string; + options: string[]; + changeRegion: (name: string) => void; +} + +export const WaffleRegionControls = (props: Props) => { + const { region, options } = props; + const [isOpen, setIsOpen] = useState(false); + + const showPopover = useCallback(() => { + setIsOpen(true); + }, [setIsOpen]); + + const closePopover = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const currentLabel = options.find(o => region === o); + + const changeRegion = useCallback( + (val: string) => { + if (region === val) { + props.changeRegion(''); + } else { + props.changeRegion(val); + } + closePopover(); + }, + [region, closePopover, props] + ); + + const panels = useMemo( + () => [ + { + id: 0, + title: '', + items: options.map(o => { + const icon = o === region ? 'check' : 'empty'; + const panel = { name: o, onClick: () => changeRegion(o), icon }; + return panel; + }), + }, + ], + [changeRegion, options, region] + ); + + const button = ( + + {currentLabel || + i18n.translate('xpack.infra.waffle.region', { + defaultMessage: 'All', + })} + + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx similarity index 95% rename from x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx index 458bb674afade..7f190f21484d9 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; import React, { useCallback } from 'react'; -import { useWaffleTimeContext } from '../../pages/inventory_view/hooks/use_waffle_time'; +import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; export const WaffleTimeControls = () => { const { diff --git a/x-pack/plugins/infra/public/containers/inventory_metadata/use_inventory_meta.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts similarity index 79% rename from x-pack/plugins/infra/public/containers/inventory_metadata/use_inventory_meta.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts index 0ed1f3e35449b..b038491690a13 100644 --- a/x-pack/plugins/infra/public/containers/inventory_metadata/use_inventory_meta.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts @@ -7,13 +7,13 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { useEffect } from 'react'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; import { InventoryMetaResponseRT, InventoryMetaResponse, -} from '../../../common/http_api/inventory_meta_api'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +} from '../../../../../common/http_api/inventory_meta_api'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; export function useInventoryMeta(sourceId: string, nodeType: InventoryItemType) { const decodeResponse = (response: any) => { diff --git a/x-pack/plugins/infra/public/containers/waffle/use_snaphot.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts similarity index 83% rename from x-pack/plugins/infra/public/containers/waffle/use_snaphot.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts index 31f02f46caeda..3ec63d7b2de28 100644 --- a/x-pack/plugins/infra/public/containers/waffle/use_snaphot.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts @@ -8,14 +8,17 @@ import { useEffect } from 'react'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; import { SnapshotNodeResponseRT, SnapshotNodeResponse, SnapshotGroupBy, -} from '../../../common/http_api/snapshot_api'; -import { InventoryItemType, SnapshotMetricType } from '../../../common/inventory_models/types'; +} from '../../../../../common/http_api/snapshot_api'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; export function useSnapshot( filterQuery: string | null | undefined, diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts similarity index 90% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts index 02c079dcaddc4..f6cbb59779039 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts @@ -10,10 +10,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainter from 'constate'; -import { useUrlState } from '../../../utils/use_url_state'; -import { useSourceContext } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; -import { esKuery } from '../../../../../../../src/plugins/data/public'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { useSourceContext } from '../../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; const validateKuery = (expression: string) => { try { diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts similarity index 95% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts index 2853917d5f683..32bfe6e085b4e 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts @@ -17,9 +17,9 @@ import { SnapshotMetricInputRT, SnapshotGroupByRT, SnapshotCustomMetricInputRT, -} from '../../../../common/http_api/snapshot_api'; -import { useUrlState } from '../../../utils/use_url_state'; -import { InventoryItemType, ItemTypeRT } from '../../../../common/inventory_models/types'; +} from '../../../../../common/http_api/snapshot_api'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { InventoryItemType, ItemTypeRT } from '../../../../../common/inventory_models/types'; export const DEFAULT_WAFFLE_OPTIONS_STATE: WaffleOptionsState = { metric: { type: 'cpu' }, diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_time.ts similarity index 97% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_time.ts index 051b5e598cb75..db3abd37b58dd 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_time.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainer from 'constate'; -import { useUrlState } from '../../../utils/use_url_state'; +import { useUrlState } from '../../../../utils/use_url_state'; export const DEFAULT_WAFFLE_TIME_STATE: WaffleTimeState = { currentTime: Date.now(), diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_view_state.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts similarity index 100% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_view_state.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx new file mode 100644 index 0000000000000..3a2c33d1c824c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -0,0 +1,105 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; + +import { FilterBar } from './components/filter_bar'; + +import { DocumentTitle } from '../../../components/document_title'; +import { NoIndices } from '../../../components/empty_states/no_indices'; +import { ColumnarPage } from '../../../components/page'; + +import { SourceErrorPage } from '../../../components/source_error_page'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; +import { Source } from '../../../containers/source'; +import { useTrackPageview } from '../../../../../observability/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { Layout } from './components/layout'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const SnapshotPage = () => { + const uiCapabilities = useKibana().services.application?.capabilities; + const { + hasFailedLoadingSource, + isLoading, + loadSourceFailureMessage, + loadSource, + metricIndicesExist, + } = useContext(Source.Context); + useTrackPageview({ app: 'infra_metrics', path: 'inventory' }); + useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 }); + + const tutorialLinkProps = useLinkProps({ + app: 'kibana', + hash: '/home/tutorial_directory/metrics', + }); + + return ( + + + i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { + defaultMessage: '{previousTitle} | Inventory', + values: { + previousTitle, + }, + }) + } + /> + {isLoading ? ( + + ) : metricIndicesExist ? ( + <> + + + + ) : hasFailedLoadingSource ? ( + + ) : ( + + + + {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', { + defaultMessage: 'View setup instructions', + })} + + + {uiCapabilities?.infrastructure?.configureSource ? ( + + + {i18n.translate('xpack.infra.configureSourceActionLabel', { + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + data-test-subj="noMetricsIndicesPrompt" + /> + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/apply_wafflemap_layout.ts similarity index 94% rename from x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/apply_wafflemap_layout.ts index 5f3c06fcfbba7..68600ac5d2ce4 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/apply_wafflemap_layout.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { first, sortBy } from 'lodash'; -import { - isWaffleMapGroupWithGroups, - isWaffleMapGroupWithNodes, -} from '../../../containers/waffle/type_guards'; -import { InfraWaffleMapGroup } from '../../../lib/lib'; +import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from './type_guards'; +import { InfraWaffleMapGroup } from '../../../../lib/lib'; import { sizeOfSquares } from './size_of_squares'; export function getColumns(n: number, w = 1, h = 1) { diff --git a/x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/color_from_value.ts similarity index 98% rename from x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/color_from_value.ts index c082686bb9d63..334865306ee88 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/color_from_value.ts @@ -12,7 +12,7 @@ import { InfraWaffleMapLegend, InfraWaffleMapRuleOperator, InfraWaffleMapStepLegend, -} from '../../../lib/lib'; +} from '../../../../lib/lib'; import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './type_guards'; const OPERATOR_TO_FN = { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts new file mode 100644 index 0000000000000..acd71e5137694 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -0,0 +1,89 @@ +/* + * 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 { get } from 'lodash'; +import { createFormatter } from '../../../../utils/formatters'; +import { InfraFormatterType } from '../../../../lib/lib'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInputRT, +} from '../../../../../common/http_api/snapshot_api'; +import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric'; + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +const METRIC_FORMATTERS: MetricFormatters = { + ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, + ['cpu']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['memory']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['logRate']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, + ['diskIOReadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['diskIOWriteBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['s3BucketSize']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3TotalRequests']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3NumberOfObjects']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3UploadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3DownloadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['sqsOldestMessage']: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, +}; + +export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => ( + val: string | number +) => { + if (SnapshotCustomMetricInputRT.is(metric)) { + const formatter = createFormatterForMetric(metric); + return formatter(val); + } + const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); + if (val == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(val); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts index 902969c83ba39..5f760cf2f591e 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts @@ -5,8 +5,8 @@ */ import { createUptimeLink } from './create_uptime_link'; -import { InfraWaffleMapOptions, InfraFormatterType } from '../../../lib/lib'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { InfraWaffleMapOptions, InfraFormatterType } from '../../../../lib/lib'; +import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; const options: InfraWaffleMapOptions = { fields: { diff --git a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts similarity index 83% rename from x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts index 72b46f4fb5c7b..6c089ee8d22f4 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts @@ -5,9 +5,9 @@ */ import { get } from 'lodash'; -import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../lib/lib'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { LinkDescriptor } from '../../../hooks/use_link_props'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { LinkDescriptor } from '../../../../hooks/use_link_props'; export const createUptimeLink = ( options: InfraWaffleMapOptions, diff --git a/x-pack/plugins/infra/public/components/waffle/lib/field_to_display_name.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/field_to_display_name.ts similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/lib/field_to_display_name.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/field_to_display_name.ts diff --git a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts similarity index 97% rename from x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts index d2511ba7e669e..469b54b2d9d68 100644 --- a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts @@ -11,9 +11,9 @@ import { InfraWaffleMapGroupOfGroups, InfraWaffleMapGroupOfNodes, InfraWaffleMapNode, -} from '../../lib/lib'; +} from '../../../../lib/lib'; import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from './type_guards'; -import { SnapshotNodePath, SnapshotNode } from '../../../common/http_api/snapshot_api'; +import { SnapshotNodePath, SnapshotNode } from '../../../../../common/http_api/snapshot_api'; export function createId(path: SnapshotNodePath[]) { return path.map(p => p.value).join('/'); diff --git a/x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/size_of_squares.ts similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/size_of_squares.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/type_guards.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/type_guards.ts new file mode 100644 index 0000000000000..62d02022d4c93 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/type_guards.ts @@ -0,0 +1,30 @@ +/* + * 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 { + InfraWaffleMapGroupOfGroups, + InfraWaffleMapGroupOfNodes, + InfraWaffleMapGradientLegend, + InfraWaffleMapStepLegend, +} from '../../../../lib/lib'; + +export function isInfraWaffleMapStepLegend(subject: any): subject is InfraWaffleMapStepLegend { + return subject.type && subject.type === 'step'; +} + +export function isInfraWaffleMapGradientLegend( + subject: any +): subject is InfraWaffleMapGradientLegend { + return subject.type && subject.type === 'gradient'; +} + +export function isWaffleMapGroupWithNodes(subject: any): subject is InfraWaffleMapGroupOfNodes { + return subject && subject.nodes != null && Array.isArray(subject.nodes); +} + +export function isWaffleMapGroupWithGroups(subject: any): subject is InfraWaffleMapGroupOfGroups { + return subject && subject.groups != null && Array.isArray(subject.groups); +} diff --git a/x-pack/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx similarity index 94% rename from x-pack/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx index 290f0eda452ce..588a0d84918c6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx @@ -8,7 +8,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { Axis, Chart, niceTimeFormatter, Position, Settings, TooltipValue } from '@elastic/charts'; import { EuiPageContentBody } from '@elastic/eui'; -import { getChartTheme } from '../../../components/metrics_explorer/helpers/get_chart_theme'; +import { getChartTheme } from '../../metrics_explorer/components/helpers/get_chart_theme'; import { SeriesChart } from './series_chart'; import { getFormatter, @@ -19,8 +19,8 @@ import { seriesHasLessThen2DataPoints, } from './helpers'; import { ErrorMessage } from './error_message'; -import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; -import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; +import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; import { VisSectionProps } from '../types'; export const ChartSectionVis = ({ diff --git a/x-pack/plugins/infra/public/pages/metrics/components/error_message.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/error_message.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/components/error_message.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/error_message.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/components/gauges_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx similarity index 92% rename from x-pack/plugins/infra/public/pages/metrics/components/gauges_section_vis.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx index e069b52be8be7..0aab676b7d6c5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/gauges_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx @@ -16,9 +16,9 @@ import { import { get, last, max } from 'lodash'; import React, { ReactText } from 'react'; -import { euiStyled } from '../../../../../observability/public'; -import { createFormatter } from '../../../utils/formatters'; -import { InventoryFormatterType } from '../../../../common/inventory_models/types'; +import { euiStyled } from '../../../../../../observability/public'; +import { createFormatter } from '../../../../utils/formatters'; +import { InventoryFormatterType } from '../../../../../common/inventory_models/types'; import { SeriesOverrides, VisSectionProps } from '../types'; import { getChartName } from './helpers'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts new file mode 100644 index 0000000000000..bb4ad32660952 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts @@ -0,0 +1,99 @@ +/* + * 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 { ReactText } from 'react'; +import Color from 'color'; +import { get, first, last, min, max } from 'lodash'; +import { createFormatter } from '../../../../utils/formatters'; +import { InfraDataSeries } from '../../../../graphql/types'; +import { + InventoryVisTypeRT, + InventoryFormatterType, + InventoryVisType, +} from '../../../../../common/inventory_models/types'; +import { SeriesOverrides } from '../types'; +import { NodeDetailsMetricData } from '../../../../../common/http_api/node_details_api'; + +/** + * Returns a formatter + */ +export const getFormatter = ( + formatter: InventoryFormatterType = 'number', + template: string = '{{value}}' +) => (val: ReactText) => (val != null ? createFormatter(formatter, template)(val) : ''); + +/** + * Does a series have more then two points? + */ +export const seriesHasLessThen2DataPoints = (series: InfraDataSeries): boolean => { + return series.data.length < 2; +}; + +/** + * Returns the minimum and maximum timestamp for a metric + */ +export const getMaxMinTimestamp = (metric: NodeDetailsMetricData): [number, number] => { + if (metric.series.some(seriesHasLessThen2DataPoints)) { + return [0, 0]; + } + const values = metric.series.reduce((acc, item) => { + const firstRow = first(item.data); + const lastRow = last(item.data); + return acc.concat([(firstRow && firstRow.timestamp) || 0, (lastRow && lastRow.timestamp) || 0]); + }, [] as number[]); + return [min(values), max(values)]; +}; + +/** + * Returns the chart name from the visConfig based on the series id, otherwise it + * just returns the seriesId + */ +export const getChartName = ( + seriesOverrides: SeriesOverrides | undefined, + seriesId: string, + label: string +) => { + if (!seriesOverrides) { + return label; + } + return get(seriesOverrides, [seriesId, 'name'], label); +}; + +/** + * Returns the chart color from the visConfig based on the series id, otherwise it + * just returns null if the color doesn't exists in the overrides. + */ +export const getChartColor = (seriesOverrides: SeriesOverrides | undefined, seriesId: string) => { + const rawColor: string | null = seriesOverrides + ? get(seriesOverrides, [seriesId, 'color']) + : null; + if (!rawColor) { + return null; + } + const color = new Color(rawColor); + return color.hex().toString(); +}; + +/** + * Gets the chart type based on the section and seriesId + */ +export const getChartType = ( + seriesOverrides: SeriesOverrides | undefined, + type: InventoryVisType | undefined, + seriesId: string +) => { + if (!seriesOverrides || !type) { + return 'line'; + } + const overrideValue = get(seriesOverrides, [seriesId, 'type']); + if (InventoryVisTypeRT.is(overrideValue)) { + return overrideValue; + } + if (InventoryVisTypeRT.is(type)) { + return type; + } + return 'line'; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx similarity index 90% rename from x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index b089e2237c2e5..6d83017a3f689 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -7,9 +7,9 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; -import { useLinkProps } from '../../../hooks/use_link_props'; +import { euiStyled } from '../../../../../../observability/public'; +import { ViewSourceConfigurationButton } from '../../../../components/source_configuration'; +import { useLinkProps } from '../../../../hooks/use_link_props'; interface InvalidNodeErrorProps { nodeName: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/layout_content.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx similarity index 84% rename from x-pack/plugins/infra/public/pages/metrics/components/layout_content.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx index 4e10f245acdcc..4620102517549 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/layout_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx @@ -5,7 +5,7 @@ */ import { EuiPageContent } from '@elastic/eui'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; export const LayoutContent = euiStyled(EuiPageContent)` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/metadata_details.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx similarity index 97% rename from x-pack/plugins/infra/public/pages/metrics/components/metadata_details.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx index 3bf492d398f2c..7ca69dd56251d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/metadata_details.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx @@ -8,8 +8,8 @@ import React, { useContext, useState, useCallback, useMemo } from 'react'; import { EuiButtonIcon, EuiFlexGrid, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { InfraMetadata } from '../../../../common/http_api'; -import { euiStyled } from '../../../../../observability/public'; +import { InfraMetadata } from '../../../../../common/http_api'; +import { euiStyled } from '../../../../../../observability/public'; import { MetadataContext } from '../containers/metadata_context'; interface FieldDef { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx similarity index 90% rename from x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx index dd2a5f2bdb39e..0d0bc8c82397e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx @@ -13,19 +13,19 @@ import { EuiHideFor, EuiTitle, } from '@elastic/eui'; -import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; -import { InventoryMetric, InventoryItemType } from '../../../../common/inventory_models/types'; -import { useNodeDetails } from '../../../containers/node_details/use_node_details'; +import { InfraTimerangeInput } from '../../../../../common/http_api/snapshot_api'; +import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types'; +import { useNodeDetails } from '../hooks/use_node_details'; import { MetricsSideNav } from './side_nav'; -import { AutoSizer } from '../../../components/auto_sizer'; +import { AutoSizer } from '../../../../components/auto_sizer'; import { MetricsTimeControls } from './time_controls'; import { SideNavContext, NavItem } from '../lib/side_nav_context'; import { PageBody } from './page_body'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; -import { InfraMetadata } from '../../../../common/http_api/metadata_api'; +import { InfraMetadata } from '../../../../../common/http_api/metadata_api'; import { PageError } from './page_error'; -import { MetadataContext } from '../../../pages/metrics/containers/metadata_context'; +import { MetadataContext } from '../containers/metadata_context'; interface Props { name: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_body.tsx similarity index 82% rename from x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_body.tsx index e651d6b92d981..68166a6e53bfd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_body.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { findLayout } from '../../../../common/inventory_models/layouts'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { findLayout } from '../../../../../common/inventory_models/layouts'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; -import { InfraLoadingPanel } from '../../../components/loading'; -import { NoData } from '../../../components/empty_states'; -import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; +import { InfraLoadingPanel } from '../../../../components/loading'; +import { NoData } from '../../../../components/empty_states'; +import { NodeDetailsMetricData } from '../../../../../common/http_api/node_details_api'; interface Props { loading: boolean; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/page_error.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx similarity index 90% rename from x-pack/plugins/infra/public/pages/metrics/components/page_error.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx index e54cdcd151f6f..bda2a5941e023 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/page_error.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { IHttpFetchError } from 'src/core/public'; import { InvalidNodeError } from './invalid_node'; // import { InfraMetricsErrorCodes } from '../../../../common/errors'; -import { DocumentTitle } from '../../../components/document_title'; -import { ErrorPageBody } from '../../error'; +import { DocumentTitle } from '../../../../components/document_title'; +import { ErrorPageBody } from '../../../error'; interface Props { name: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/section.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/section.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/components/section.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/section.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx new file mode 100644 index 0000000000000..0d7716ad3cc66 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx @@ -0,0 +1,87 @@ +/* + * 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 { + AreaSeries, + BarSeries, + ScaleType, + RecursivePartial, + BarSeriesStyle, + AreaSeriesStyle, +} from '@elastic/charts'; +import { InfraDataSeries } from '../../../../graphql/types'; +import { InventoryVisType } from '../../../../../common/inventory_models/types'; + +interface Props { + id: string; + name: string; + color: string | null; + series: InfraDataSeries; + type: InventoryVisType; + stack: boolean | undefined; +} + +export const SeriesChart = (props: Props) => { + if (props.type === 'bar') { + return ; + } + return ; +}; + +export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { + const style: RecursivePartial = { + area: { + opacity: 1, + visible: 'area' === type, + }, + line: { + strokeWidth: 'area' === type ? 1 : 2, + visible: true, + }, + }; + return ( + + ); +}; + +export const BarChart = ({ id, color, series, name, type, stack }: Props) => { + const style: RecursivePartial = { + rectBorder: { + stroke: color || void 0, + strokeWidth: 1, + visible: true, + }, + rect: { + opacity: 1, + }, + }; + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/side_nav.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx similarity index 95% rename from x-pack/plugins/infra/public/pages/metrics/components/side_nav.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx index 94f97c7f45e61..1cba3366acbbb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/side_nav.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx @@ -6,7 +6,7 @@ import { EuiHideFor, EuiPageSideBar, EuiShowFor, EuiSideNav } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; import { NavItem } from '../lib/side_nav_context'; interface Props { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/sub_section.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx similarity index 94% rename from x-pack/plugins/infra/public/pages/metrics/components/sub_section.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx index 325d510293135..7b269adc96638 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/sub_section.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx @@ -6,7 +6,7 @@ import React, { isValidElement, cloneElement, FunctionComponent, Children, useMemo } from 'react'; import { EuiTitle } from '@elastic/eui'; -import { InventoryMetric } from '../../../../common/inventory_models/types'; +import { InventoryMetric } from '../../../../../common/inventory_models/types'; import { LayoutProps } from '../types'; type SubSectionProps = LayoutProps & { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.test.tsx index 02ba506e8abe1..83f5187f8a46c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../utils/use_kibana_ui_setting', () => ({ +jest.mock('../../../../utils/use_kibana_ui_setting', () => ({ _esModule: true, useKibanaUiSetting: jest.fn(() => [ [ diff --git a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx similarity index 91% rename from x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx index cdbdc9bb7ecdb..ef6486eac0fdb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx @@ -6,10 +6,10 @@ import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; -import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; -import { mapKibanaQuickRangesToDatePickerRanges } from '../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; +import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; interface MetricsTimeControlsProps { currentTimeRange: MetricsTimeInput; diff --git a/x-pack/plugins/infra/public/pages/metrics/containers/metadata_context.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/containers/metadata_context.ts similarity index 84% rename from x-pack/plugins/infra/public/pages/metrics/containers/metadata_context.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/containers/metadata_context.ts index 4ecf7fa15548c..b8995d27d4dbe 100644 --- a/x-pack/plugins/infra/public/pages/metrics/containers/metadata_context.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/containers/metadata_context.ts @@ -5,5 +5,5 @@ */ import React from 'react'; -import { InfraMetadata } from '../../../../common/http_api'; +import { InfraMetadata } from '../../../../../common/http_api'; export const MetadataContext = React.createContext(null); diff --git a/x-pack/plugins/infra/public/pages/metrics/hooks/metrics_time.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/metrics_time.test.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/hooks/metrics_time.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/metrics_time.test.tsx diff --git a/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metadata.ts similarity index 76% rename from x-pack/plugins/infra/public/containers/metadata/use_metadata.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metadata.ts index 1ba016195bef4..d2a20d449d89a 100644 --- a/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metadata.ts @@ -8,12 +8,12 @@ import { useEffect } from 'react'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { InfraMetadata, InfraMetadataRT } from '../../../common/http_api/metadata_api'; -import { useHTTPRequest } from '../../hooks/use_http_request'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { InventoryMetric, InventoryItemType } from '../../../common/inventory_models/types'; -import { getFilteredMetrics } from './lib/get_filtered_metrics'; -import { MetricsTimeInput } from '../../pages/metrics/hooks/use_metrics_time'; +import { InfraMetadata, InfraMetadataRT } from '../../../../../common/http_api/metadata_api'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types'; +import { getFilteredMetrics } from '../lib/get_filtered_metrics'; +import { MetricsTimeInput } from './use_metrics_time'; export function useMetadata( nodeId: string, diff --git a/x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metrics_time.ts similarity index 96% rename from x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metrics_time.ts index 2ed86863535ff..98803ef2e69c6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metrics_time.ts @@ -12,8 +12,8 @@ import * as rt from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; -import { useUrlState } from '../../../utils/use_url_state'; -import { replaceStateKeyInQueryString } from '../../../utils/url_state'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { replaceStateKeyInQueryString } from '../../../../utils/url_state'; const parseRange = (range: MetricsTimeInput) => { const parsedFrom = dateMath.parse(range.from.toString()); diff --git a/x-pack/plugins/infra/public/containers/node_details/use_node_details.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_node_details.ts similarity index 75% rename from x-pack/plugins/infra/public/containers/node_details/use_node_details.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_node_details.ts index 189c48ba1a70c..3bb6c98c8eac0 100644 --- a/x-pack/plugins/infra/public/containers/node_details/use_node_details.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_node_details.ts @@ -6,14 +6,14 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; import { NodeDetailsMetricDataResponseRT, NodeDetailsMetricDataResponse, -} from '../../../common/http_api/node_details_api'; -import { InventoryMetric, InventoryItemType } from '../../../common/inventory_models/types'; -import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; +} from '../../../../../common/http_api/node_details_api'; +import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types'; +import { InfraTimerangeInput } from '../../../../../common/http_api/snapshot_api'; export function useNodeDetails( metrics: InventoryMetric[], diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx new file mode 100644 index 0000000000000..197a735f7fd1f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -0,0 +1,140 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useContext, useState } from 'react'; +import { euiStyled, EuiTheme, withTheme } from '../../../../../observability/public'; +import { DocumentTitle } from '../../../components/document_title'; +import { Header } from '../../../components/header'; +import { ColumnarPage, PageContent } from '../../../components/page'; +import { withMetricPageProviders } from './page_providers'; +import { useMetadata } from './hooks/use_metadata'; +import { Source } from '../../../containers/source'; +import { InfraLoadingPanel } from '../../../components/loading'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { NavItem } from './lib/side_nav_context'; +import { NodeDetailsPage } from './components/node_details_page'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { useMetricsTimeContext } from './hooks/use_metrics_time'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +const DetailPageContent = euiStyled(PageContent)` + overflow: auto; + background-color: ${props => props.theme.eui.euiColorLightestShade}; +`; + +interface Props { + theme: EuiTheme; + match: { + params: { + type: string; + node: string; + }; + }; +} + +export const MetricDetail = withMetricPageProviders( + withTheme(({ match }: Props) => { + const uiCapabilities = useKibana().services.application?.capabilities; + const nodeId = match.params.node; + const nodeType = match.params.type as InventoryItemType; + const inventoryModel = findInventoryModel(nodeType); + const { sourceId } = useContext(Source.Context); + const { + timeRange, + parsedTimeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + triggerRefresh, + } = useMetricsTimeContext(); + const { + name, + filteredRequiredMetrics, + loading: metadataLoading, + cloudId, + metadata, + } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); + + const [sideNav, setSideNav] = useState([]); + + const addNavItem = React.useCallback( + (item: NavItem) => { + if (!sideNav.some(n => n.id === item.id)) { + setSideNav([item, ...sideNav]); + } + }, + [sideNav] + ); + + const metricsLinkProps = useLinkProps({ + app: 'metrics', + pathname: '/', + }); + + const breadcrumbs = [ + { + ...metricsLinkProps, + text: i18n.translate('xpack.infra.header.infrastructureTitle', { + defaultMessage: 'Metrics', + }), + }, + { text: name }, + ]; + + if (metadataLoading && !filteredRequiredMetrics.length) { + return ( + + ); + } + + return ( + +
+ + + {metadata ? ( + + ) : null} + + + ); + }) +); diff --git a/x-pack/plugins/infra/public/containers/metadata/lib/get_filtered_metrics.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts similarity index 78% rename from x-pack/plugins/infra/public/containers/metadata/lib/get_filtered_metrics.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts index b485c90700145..57ff182f01963 100644 --- a/x-pack/plugins/infra/public/containers/metadata/lib/get_filtered_metrics.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraMetadataFeature } from '../../../../common/http_api/metadata_api'; -import { InventoryMetric } from '../../../../common/inventory_models/types'; -import { metrics } from '../../../../common/inventory_models/metrics'; +import { InfraMetadataFeature } from '../../../../../common/http_api/metadata_api'; +import { InventoryMetric } from '../../../../../common/inventory_models/types'; +import { metrics } from '../../../../../common/inventory_models/metrics'; export const getFilteredMetrics = ( requiredMetrics: InventoryMetric[], diff --git a/x-pack/plugins/infra/public/pages/metrics/lib/side_nav_context.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/side_nav_context.ts similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/lib/side_nav_context.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/side_nav_context.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx similarity index 91% rename from x-pack/plugins/infra/public/pages/metrics/page_providers.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx index d3f10adec06ed..597977d9d2735 100644 --- a/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { Source } from '../../containers/source'; +import { Source } from '../../../containers/source'; import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const withMetricPageProviders = (Component: React.ComponentType) => ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts new file mode 100644 index 0000000000000..3ec57e23a425d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts @@ -0,0 +1,62 @@ +/* + * 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 rt from 'io-ts'; +import { EuiTheme } from '../../../../../observability/public'; +import { InventoryFormatterTypeRT } from '../../../../common/inventory_models/types'; +import { MetricsTimeInput } from './hooks/use_metrics_time'; +import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; + +export interface LayoutProps { + metrics?: NodeDetailsMetricData[]; + onChangeRangeTime?: (time: MetricsTimeInput) => void; + isLiveStreaming?: boolean; + stopLiveStreaming?: () => void; +} + +export type LayoutPropsWithTheme = LayoutProps & { theme: EuiTheme }; + +const ChartTypesRT = rt.keyof({ + area: null, + bar: null, + line: null, +}); + +export const SeriesOverridesObjectRT = rt.intersection([ + rt.type({ + color: rt.string, + }), + rt.partial({ + name: rt.string, + formatter: InventoryFormatterTypeRT, + formatterTemplate: rt.string, + gaugeMax: rt.number, + type: ChartTypesRT, + }), +]); + +export const SeriesOverridesRT = rt.record( + rt.string, + rt.union([rt.undefined, SeriesOverridesObjectRT]) +); + +export type SeriesOverrides = rt.TypeOf; + +export const VisSectionPropsRT = rt.partial({ + type: ChartTypesRT, + stacked: rt.boolean, + formatter: InventoryFormatterTypeRT, + formatterTemplate: rt.string, + seriesOverrides: SeriesOverridesRT, +}); + +export type VisSectionProps = rt.TypeOf & { + id?: string; + metric?: NodeDetailsMetricData; + onChangeRangeTime?: (time: MetricsTimeInput) => void; + isLiveStreaming?: boolean; + stopLiveStreaming?: () => void; +}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/aggregation.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx similarity index 89% rename from x-pack/plugins/infra/public/components/metrics_explorer/aggregation.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx index 76fa519ab3756..0c0f7b33b3a4a 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/aggregation.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx @@ -8,12 +8,12 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; -import { MetricsExplorerAggregation } from '../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerAggregation } from '../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; import { metricsExplorerAggregationRT, METRIC_EXPLORER_AGGREGATIONS, -} from '../../../common/http_api/metrics_explorer'; +} from '../../../../../common/http_api/metrics_explorer'; interface Props { options: MetricsExplorerOptions; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/metrics_explorer/chart.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 43b08f45eed34..089e1abfc4c91 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -10,24 +10,24 @@ import { EuiTitle, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Axis, Chart, niceTimeFormatter, Position, Settings, TooltipValue } from '@elastic/charts'; import { first, last } from 'lodash'; import moment from 'moment'; -import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; +import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions, MetricsExplorerYAxisMode, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { euiStyled } from '../../../../observability/public'; +} from '../hooks/use_metrics_explorer_options'; +import { euiStyled } from '../../../../../../observability/public'; import { createFormatterForMetric } from './helpers/create_formatter_for_metric'; import { MetricExplorerSeriesChart } from './series_chart'; import { MetricsExplorerChartContextMenu } from './chart_context_menu'; -import { SourceQuery } from '../../graphql/types'; +import { SourceQuery } from '../../../../graphql/types'; import { MetricsExplorerEmptyChart } from './empty_chart'; import { MetricsExplorerNoMetrics } from './no_metrics'; import { getChartTheme } from './helpers/get_chart_theme'; -import { useKibanaUiSetting } from '../../utils/use_kibana_ui_setting'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; import { calculateDomain } from './helpers/calculate_domain'; -import { useKibana, useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana, useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; interface Props { title?: string | null; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx index 8ffef269a42ea..5c0abb8fd845f 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx @@ -7,9 +7,14 @@ import React from 'react'; import { MetricsExplorerChartContextMenu, createNodeDetailLink, Props } from './chart_context_menu'; import { ReactWrapper, mount } from 'enzyme'; -import { options, source, timeRange, chartOptions } from '../../utils/fixtures/metrics_explorer'; +import { + options, + source, + timeRange, + chartOptions, +} from '../../../../utils/fixtures/metrics_explorer'; import { Capabilities } from 'src/core/public'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { coreMock } from 'src/core/public/mocks'; const coreStartMock = coreMock.createStart(); diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx similarity index 91% rename from x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index 75a04cbe9799e..31086a21ca13f 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -14,18 +14,18 @@ import { } from '@elastic/eui'; import DateMath from '@elastic/datemath'; import { Capabilities } from 'src/core/public'; -import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; +import { AlertFlyout } from '../../../../components/alerting/metrics/alert_flyout'; +import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../hooks/use_metrics_explorer_options'; import { createTSVBLink } from './helpers/create_tsvb_link'; -import { getNodeDetailUrl } from '../../pages/link_to/redirect_to_node_detail'; -import { SourceConfiguration } from '../../utils/source_configuration'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { AlertFlyout } from '../alerting/metrics/alert_flyout'; -import { useLinkProps } from '../../hooks/use_link_props'; +import { getNodeDetailUrl } from '../../../link_to/redirect_to_node_detail'; +import { SourceConfiguration } from '../../../../utils/source_configuration'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { useLinkProps } from '../../../../hooks/use_link_props'; export interface Props { options: MetricsExplorerOptions; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_options.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx similarity index 98% rename from x-pack/plugins/infra/public/components/metrics_explorer/chart_options.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx index 657c4cea30f3a..ba28075ededb6 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_options.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx @@ -19,7 +19,7 @@ import { MetricsExplorerChartOptions as ChartOptions, MetricsExplorerYAxisMode, MetricsExplorerChartType, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../hooks/use_metrics_explorer_options'; interface Props { chartOptions: ChartOptions; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/charts.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/metrics_explorer/charts.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx index 64e7b27f5722f..ecec116310875 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/charts.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx @@ -8,16 +8,16 @@ import { EuiButton, EuiFlexGrid, EuiFlexItem, EuiText, EuiHorizontalRule } from import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { MetricsExplorerResponse } from '../../../common/http_api/metrics_explorer'; +import { MetricsExplorerResponse } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { InfraLoadingPanel } from '../loading'; -import { NoData } from '../empty_states/no_data'; +} from '../hooks/use_metrics_explorer_options'; +import { InfraLoadingPanel } from '../../../../components/loading'; +import { NoData } from '../../../../components/empty_states/no_data'; import { MetricsExplorerChart } from './chart'; -import { SourceQuery } from '../../graphql/types'; +import { SourceQuery } from '../../../../graphql/types'; interface Props { loading: boolean; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/empty_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/empty_chart.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/metrics_explorer/empty_chart.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/empty_chart.tsx diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/group_by.tsx similarity index 93% rename from x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/group_by.tsx index 3246a2aa4a731..bfe8ddb2e0829 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/group_by.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; interface Props { options: MetricsExplorerOptions; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domain.ts similarity index 87% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domain.ts index 90569854b833b..811486d355f2e 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domain.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { min, max, sum, isNumber } from 'lodash'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptionsMetric } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; const getMin = (values: Array) => { const minValue = min(values); diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts similarity index 85% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts index 4b45534d41db8..f94c6b6156ae4 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts @@ -5,9 +5,9 @@ */ import { calculateDomain } from './calculate_domain'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptionsMetric } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { MetricsExplorerColor } from '../../../../common/color_palette'; +import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; +import { MetricsExplorerColor } from '../../../../../../common/color_palette'; describe('calculateDomain()', () => { const series: MetricsExplorerSeries = { id: 'test-01', diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metric.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts similarity index 76% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metric.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts index 33ec2ce2715a3..d07a6b45f02be 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metric.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -import { createFormatter } from '../../../utils/formatters'; -import { InfraFormatterType } from '../../../lib/lib'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { createFormatter } from '../../../../../utils/formatters'; +import { InfraFormatterType } from '../../../../../lib/lib'; import { metricToFormat } from './metric_to_format'; export const createFormatterForMetric = (metric?: MetricsExplorerMetric) => { if (metric && metric.field) { diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metrics.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metrics.test.ts similarity index 94% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metrics.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metrics.test.ts index ec41e90e441a4..e039d5d4b3eeb 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metrics.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metrics.test.ts @@ -5,7 +5,7 @@ */ import { createFormatterForMetric } from './create_formatter_for_metric'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; describe('createFormatterForMetric()', () => { it('should just work for count', () => { diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.test.ts similarity index 88% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.test.ts index cbf6904d246c7..367c472f414e4 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.test.ts @@ -5,7 +5,7 @@ */ import { createMetricLabel } from './create_metric_label'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; describe('createMetricLabel()', () => { it('should work with metrics with fields', () => { diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts similarity index 80% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts index b6453a81317b1..1607302a6259a 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; export const createMetricLabel = (metric: MetricsExplorerMetric) => { return `${metric.aggregation}(${metric.field || ''})`; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts similarity index 97% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index 05637642b8dd9..47bb4c8c57716 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -5,14 +5,19 @@ */ import { createTSVBLink, createFilterFromOptions } from './create_tsvb_link'; -import { source, options, timeRange, chartOptions } from '../../../utils/fixtures/metrics_explorer'; +import { + source, + options, + timeRange, + chartOptions, +} from '../../../../../utils/fixtures/metrics_explorer'; import uuid from 'uuid'; import { OutputBuffer } from 'uuid/interfaces'; import { MetricsExplorerYAxisMode, MetricsExplorerChartType, -} from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../hooks/use_metrics_explorer_options'; +import { MetricsExplorerOptions } from '../../hooks/use_metrics_explorer_options'; jest.mock('uuid'); const mockedUuid = uuid as jest.Mocked; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts similarity index 91% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 20706f563ec63..559422584f579 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -7,8 +7,8 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; import { set } from 'lodash'; -import { colorTransformer, MetricsExplorerColor } from '../../../../common/color_palette'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; +import { colorTransformer, MetricsExplorerColor } from '../../../../../../common/color_palette'; +import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerOptionsMetric, @@ -16,12 +16,12 @@ import { MetricsExplorerChartOptions, MetricsExplorerYAxisMode, MetricsExplorerChartType, -} from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../hooks/use_metrics_explorer_options'; import { metricToFormat } from './metric_to_format'; -import { InfraFormatterType } from '../../../lib/lib'; -import { SourceQuery } from '../../../graphql/types'; +import { InfraFormatterType } from '../../../../../lib/lib'; +import { SourceQuery } from '../../../../../graphql/types'; import { createMetricLabel } from './create_metric_label'; -import { LinkDescriptor } from '../../../hooks/use_link_props'; +import { LinkDescriptor } from '../../../../../hooks/use_link_props'; export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptionsMetric) => { if (metric.aggregation === 'rate') { diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts similarity index 100% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.test.ts similarity index 90% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.test.ts index 4cb27b4fb65c3..6b27627a9fb5b 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.test.ts @@ -5,8 +5,8 @@ */ import { metricToFormat } from './metric_to_format'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -import { InfraFormatterType } from '../../../lib/lib'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { InfraFormatterType } from '../../../../../lib/lib'; describe('metricToFormat()', () => { it('should just work for numeric metrics', () => { const metric: MetricsExplorerMetric = { aggregation: 'avg', field: 'system.load.1' }; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.ts similarity index 82% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.ts index 63272c86a5dc7..1dbbf97a32217 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.ts @@ -5,8 +5,8 @@ */ import { last } from 'lodash'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -import { InfraFormatterType } from '../../../lib/lib'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { InfraFormatterType } from '../../../../../lib/lib'; export const metricToFormat = (metric?: MetricsExplorerMetric) => { if (metric && metric.field) { const suffix = last(metric.field.split(/\./)); diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx similarity index 88% rename from x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index 7c3e0444dbeea..e9826e1ff3955 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; -import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; -import { AutocompleteField } from '../autocomplete_field'; -import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; +import { AutocompleteField } from '../../../../components/autocomplete_field'; +import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; interface Props { derivedIndexPattern: IIndexPattern; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx similarity index 91% rename from x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx index a49e42c9cac0e..612735e2ba772 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx @@ -9,9 +9,9 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; -import { MetricsExplorerMetric } from '../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; +import { colorTransformer, MetricsExplorerColor } from '../../../../../common/color_palette'; +import { MetricsExplorerMetric } from '../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; interface Props { autoFocus?: boolean; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/no_metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/no_metrics.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/metrics_explorer/no_metrics.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/no_metrics.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx new file mode 100644 index 0000000000000..ad7ce83539526 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -0,0 +1,107 @@ +/* + * 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 { + ScaleType, + AreaSeries, + BarSeries, + RecursivePartial, + AreaSeriesStyle, + BarSeriesStyle, +} from '@elastic/charts'; +import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; +import { colorTransformer, MetricsExplorerColor } from '../../../../../common/color_palette'; +import { createMetricLabel } from './helpers/create_metric_label'; +import { + MetricsExplorerOptionsMetric, + MetricsExplorerChartType, +} from '../hooks/use_metrics_explorer_options'; + +interface Props { + metric: MetricsExplorerOptionsMetric; + id: string | number; + series: MetricsExplorerSeries; + type: MetricsExplorerChartType; + stack: boolean; +} + +export const MetricExplorerSeriesChart = (props: Props) => { + if (MetricsExplorerChartType.bar === props.type) { + return ; + } + return ; +}; + +export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Props) => { + const color = + (metric.color && colorTransformer(metric.color)) || + colorTransformer(MetricsExplorerColor.color0); + + const yAccessor = `metric_${id}`; + const chartId = `series-${series.id}-${yAccessor}`; + + const seriesAreaStyle: RecursivePartial = { + line: { + strokeWidth: 2, + visible: true, + }, + area: { + opacity: 0.5, + visible: type === MetricsExplorerChartType.area, + }, + }; + return ( + + ); +}; + +export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => { + const color = + (metric.color && colorTransformer(metric.color)) || + colorTransformer(MetricsExplorerColor.color0); + + const yAccessor = `metric_${id}`; + const chartId = `series-${series.id}-${yAccessor}`; + + const seriesBarStyle: RecursivePartial = { + rectBorder: { + stroke: color, + strokeWidth: 1, + visible: true, + }, + rect: { + opacity: 1, + }, + }; + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx new file mode 100644 index 0000000000000..6913f67bad08a --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -0,0 +1,148 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { IIndexPattern } from 'src/plugins/data/public'; +import { + MetricsExplorerMetric, + MetricsExplorerAggregation, +} from '../../../../../common/http_api/metrics_explorer'; +import { + MetricsExplorerOptions, + MetricsExplorerTimeOptions, + MetricsExplorerChartOptions, +} from '../hooks/use_metrics_explorer_options'; +import { MetricsExplorerKueryBar } from './kuery_bar'; +import { MetricsExplorerMetrics } from './metrics'; +import { MetricsExplorerGroupBy } from './group_by'; +import { MetricsExplorerAggregationPicker } from './aggregation'; +import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options'; +import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state'; +import { metricsExplorerViewSavedObjectType } from '../../../../../common/saved_objects/metrics_explorer_view'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; +import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; + +interface Props { + derivedIndexPattern: IIndexPattern; + timeRange: MetricsExplorerTimeOptions; + options: MetricsExplorerOptions; + chartOptions: MetricsExplorerChartOptions; + defaultViewState: MetricExplorerViewState; + onRefresh: () => void; + onTimeChange: (start: string, end: string) => void; + onGroupByChange: (groupBy: string | null) => void; + onFilterQuerySubmit: (query: string) => void; + onMetricsChange: (metrics: MetricsExplorerMetric[]) => void; + onAggregationChange: (aggregation: MetricsExplorerAggregation) => void; + onChartOptionsChange: (chartOptions: MetricsExplorerChartOptions) => void; + onViewStateChange: (vs: MetricExplorerViewState) => void; +} + +export const MetricsExplorerToolbar = ({ + timeRange, + derivedIndexPattern, + options, + onTimeChange, + onRefresh, + onGroupByChange, + onFilterQuerySubmit, + onMetricsChange, + onAggregationChange, + chartOptions, + onChartOptionsChange, + defaultViewState, + onViewStateChange, +}: Props) => { + const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0; + const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); + const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); + + return ( + + + + + + {options.aggregation !== 'count' && ( + + + + )} + {options.aggregation !== 'count' && ( + + + + )} + + + + + + + + + + + + + + + + + + + + onTimeChange(start, end)} + onRefresh={onRefresh} + commonlyUsedRanges={commonlyUsedRanges} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx index 874ac0987023b..f0734f76cfacd 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx @@ -6,14 +6,14 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useMetricsExplorerState } from './use_metric_explorer_state'; -import { MetricsExplorerOptionsContainer } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerOptionsContainer } from './use_metrics_explorer_options'; import React from 'react'; import { source, derivedIndexPattern, resp, createSeries, -} from '../../../utils/fixtures/metrics_explorer'; +} from '../../../../utils/fixtures/metrics_explorer'; const renderUseMetricsExplorerStateHook = () => renderHook(props => useMetricsExplorerState(props.source, props.derivedIndexPattern), { @@ -27,7 +27,7 @@ const renderUseMetricsExplorerStateHook = () => const mockedUseMetricsExplorerData = jest.fn(); -jest.mock('../../../containers/metrics_explorer/use_metrics_explorer_data', () => { +jest.mock('./use_metrics_explorer_data', () => { return { useMetricsExplorerData: () => { return mockedUseMetricsExplorerData(); diff --git a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts similarity index 92% rename from x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index 88e6d9d800661..8a9ed901de0b0 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -9,15 +9,15 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { MetricsExplorerMetric, MetricsExplorerAggregation, -} from '../../../../common/http_api/metrics_explorer'; -import { useMetricsExplorerData } from '../../../containers/metrics_explorer/use_metrics_explorer_data'; +} from '../../../../../common/http_api/metrics_explorer'; +import { useMetricsExplorerData } from './use_metrics_explorer_data'; import { MetricsExplorerOptionsContainer, MetricsExplorerChartOptions, MetricsExplorerTimeOptions, MetricsExplorerOptions, -} from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { SourceQuery } from '../../../graphql/types'; +} from './use_metrics_explorer_options'; +import { SourceQuery } from '../../../../graphql/types'; export interface MetricExplorerViewState { chartOptions: MetricsExplorerChartOptions; diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx index bbc8778545b4a..94edab54fb71e 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { useMetricsExplorerData } from './use_metrics_explorer_data'; import { renderHook } from '@testing-library/react-hooks'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { options, @@ -17,7 +17,7 @@ import { timeRange, resp, createSeries, -} from '../../utils/fixtures/metrics_explorer'; +} from '../../../../utils/fixtures/metrics_explorer'; const mockedFetch = jest.fn(); @@ -54,7 +54,7 @@ const renderUseMetricsExplorerDataHook = () => { ); }; -jest.mock('../../utils/kuery', () => { +jest.mock('../../../../utils/kuery', () => { return { convertKueryToElasticSearchQuery: (query: string) => query, }; diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts similarity index 91% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index b32496fbf30a1..93aacb586a5cd 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -8,15 +8,15 @@ import DateMath from '@elastic/datemath'; import { isEqual } from 'lodash'; import { useEffect, useState } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { SourceQuery } from '../../../common/graphql/types'; +import { SourceQuery } from '../../../../../common/graphql/types'; import { MetricsExplorerResponse, metricsExplorerResponseRT, -} from '../../../common/http_api/metrics_explorer'; -import { convertKueryToElasticSearchQuery } from '../../utils/kuery'; +} from '../../../../../common/http_api/metrics_explorer'; +import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions } from './use_metrics_explorer_options'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { decodeOrThrow } from '../../../common/runtime_types'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; function isSameOptions(current: MetricsExplorerOptions, next: MetricsExplorerOptions) { return isEqual(current, next); diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx similarity index 100% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts similarity index 96% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 2b802af8e8c15..9d124a6af8012 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -6,11 +6,11 @@ import createContainer from 'constate'; import { useState, useEffect, Dispatch, SetStateAction } from 'react'; -import { MetricsExplorerColor } from '../../../common/color_palette'; +import { MetricsExplorerColor } from '../../../../../common/color_palette'; import { MetricsExplorerAggregation, MetricsExplorerMetric, -} from '../../../common/http_api/metrics_explorer'; +} from '../../../../../common/http_api/metrics_explorer'; export type MetricsExplorerOptionsMetric = MetricsExplorerMetric & { color?: MetricsExplorerColor; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx new file mode 100644 index 0000000000000..a213671e9436e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -0,0 +1,100 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import React from 'react'; +import { IIndexPattern } from 'src/plugins/data/public'; +import { DocumentTitle } from '../../../components/document_title'; +import { MetricsExplorerCharts } from './components/charts'; +import { MetricsExplorerToolbar } from './components/toolbar'; +import { SourceQuery } from '../../../../common/graphql/types'; +import { NoData } from '../../../components/empty_states'; +import { useMetricsExplorerState } from './hooks/use_metric_explorer_state'; +import { useTrackPageview } from '../../../../../observability/public'; + +interface MetricsExplorerPageProps { + source: SourceQuery.Query['source']['configuration']; + derivedIndexPattern: IIndexPattern; +} + +export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { + const { + loading, + error, + data, + currentTimerange, + options, + chartOptions, + setChartOptions, + handleAggregationChange, + handleMetricsChange, + handleFilterQuerySubmit, + handleGroupByChange, + handleTimeChange, + handleRefresh, + handleLoadMore, + defaultViewState, + onViewStateChange, + } = useMetricsExplorerState(source, derivedIndexPattern); + + useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' }); + useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); + + return ( + + + i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { + defaultMessage: '{previousTitle} | Metrics Explorer', + values: { + previousTitle, + }, + }) + } + /> + + {error ? ( + + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/infrastructure/settings.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/types.ts b/x-pack/plugins/infra/public/pages/metrics/types.ts deleted file mode 100644 index 2cc261df28977..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/types.ts +++ /dev/null @@ -1,62 +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 rt from 'io-ts'; -import { EuiTheme } from '../../../../observability/public'; -import { InventoryFormatterTypeRT } from '../../../common/inventory_models/types'; -import { MetricsTimeInput } from './hooks/use_metrics_time'; -import { NodeDetailsMetricData } from '../../../common/http_api/node_details_api'; - -export interface LayoutProps { - metrics?: NodeDetailsMetricData[]; - onChangeRangeTime?: (time: MetricsTimeInput) => void; - isLiveStreaming?: boolean; - stopLiveStreaming?: () => void; -} - -export type LayoutPropsWithTheme = LayoutProps & { theme: EuiTheme }; - -const ChartTypesRT = rt.keyof({ - area: null, - bar: null, - line: null, -}); - -export const SeriesOverridesObjectRT = rt.intersection([ - rt.type({ - color: rt.string, - }), - rt.partial({ - name: rt.string, - formatter: InventoryFormatterTypeRT, - formatterTemplate: rt.string, - gaugeMax: rt.number, - type: ChartTypesRT, - }), -]); - -export const SeriesOverridesRT = rt.record( - rt.string, - rt.union([rt.undefined, SeriesOverridesObjectRT]) -); - -export type SeriesOverrides = rt.TypeOf; - -export const VisSectionPropsRT = rt.partial({ - type: ChartTypesRT, - stacked: rt.boolean, - formatter: InventoryFormatterTypeRT, - formatterTemplate: rt.string, - seriesOverrides: SeriesOverridesRT, -}); - -export type VisSectionProps = rt.TypeOf & { - id?: string; - metric?: NodeDetailsMetricData; - onChangeRangeTime?: (time: MetricsTimeInput) => void; - isLiveStreaming?: boolean; - stopLiveStreaming?: () => void; -}; diff --git a/x-pack/plugins/infra/public/routers/metrics_router.tsx b/x-pack/plugins/infra/public/routers/metrics_router.tsx index 7cb9de65e7291..0e427150a46cc 100644 --- a/x-pack/plugins/infra/public/routers/metrics_router.tsx +++ b/x-pack/plugins/infra/public/routers/metrics_router.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { Route, Router, Switch } from 'react-router-dom'; import { NotFoundPage } from '../pages/404'; -import { InfrastructurePage } from '../pages/infrastructure'; -import { LinkToMetricsPage } from '../pages/link_to'; -import { MetricDetail } from '../pages/metrics'; +import { InfrastructurePage } from '../pages/metrics'; +import { MetricDetail } from '../pages/metrics/metric_detail'; import { RedirectWithQueryParams } from '../utils/redirect_with_query_params'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { AppRouter } from './index'; +import { LinkToMetricsPage } from '../pages/link_to'; export const MetricsRouter: AppRouter = ({ history }) => { const uiCapabilities = useKibana().services.application?.capabilities; diff --git a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts index e39d6f785d53e..15159ad45c7f3 100644 --- a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts +++ b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts @@ -14,7 +14,7 @@ import { MetricsExplorerChartType, MetricsExplorerYAxisMode, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; export const options: MetricsExplorerOptions = { limit: 3, diff --git a/x-pack/plugins/infra/public/utils/history_context.ts b/x-pack/plugins/infra/public/utils/history_context.ts index fe036e3179ec1..844d5b5e8e76f 100644 --- a/x-pack/plugins/infra/public/utils/history_context.ts +++ b/x-pack/plugins/infra/public/utils/history_context.ts @@ -5,9 +5,9 @@ */ import { createContext, useContext } from 'react'; -import { History } from 'history'; +import { ScopedHistory } from 'src/core/public'; -export const HistoryContext = createContext(undefined); +export const HistoryContext = createContext(undefined); export const useHistory = () => { return useContext(HistoryContext); diff --git a/x-pack/plugins/infra/public/utils/is_displayable.test.ts b/x-pack/plugins/infra/public/utils/is_displayable.test.ts deleted file mode 100644 index ebd5c07327e9b..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.test.ts +++ /dev/null @@ -1,65 +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 { isDisplayable } from './is_displayable'; - -describe('isDisplayable()', () => { - test('field that is not displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: false, - }; - expect(isDisplayable(field)).toBe(false); - }); - test('field that is displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that an ecs field', () => { - const field = { - name: '@timestamp', - type: 'date', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that matches same prefix', () => { - const field = { - name: 'system.network.name', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(true); - }); - test('field that does not matches same prefix', () => { - const field = { - name: 'system.load.1', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(false); - }); - test('field that is an K8s allowed field but does not match prefix', () => { - const field = { - name: 'kubernetes.namespace', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['kubernetes.pod'])).toBe(true); - }); - test('field that is a Prometheus allowed field but does not match prefix', () => { - const field = { - name: 'prometheus.labels.foo.bar', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['prometheus.metrics'])).toBe(true); - }); -}); diff --git a/x-pack/plugins/infra/public/utils/is_displayable.ts b/x-pack/plugins/infra/public/utils/is_displayable.ts deleted file mode 100644 index 534282e807036..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.ts +++ /dev/null @@ -1,30 +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 { IFieldType } from 'src/plugins/data/public'; -import { startsWith, uniq } from 'lodash'; -import { getAllowedListForPrefix } from '../../common/ecs_allowed_list'; - -interface DisplayableFieldType extends IFieldType { - displayable?: boolean; -} - -const fieldStartsWith = (field: DisplayableFieldType) => (name: string) => - startsWith(field.name, name); - -export const isDisplayable = (field: DisplayableFieldType, additionalPrefixes: string[] = []) => { - // We need to start with at least one prefix, even if it's empty - const prefixes = additionalPrefixes && additionalPrefixes.length ? additionalPrefixes : ['']; - // Create a set of allowed list based on the prefixes - const allowedList = prefixes.reduce((acc, prefix) => { - return uniq([...acc, ...getAllowedListForPrefix(prefix)]); - }, [] as string[]); - // If the field is displayable and part of the allowed list or covered by the prefix - return ( - (field.displayable && prefixes.some(fieldStartsWith(field))) || - allowedList.some(fieldStartsWith(field)) - ); -}; diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx new file mode 100644 index 0000000000000..10f8fb9e71f43 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { createContext, useContext } from 'react'; + +interface ContextValues { + prompt?: string; + setPrompt: (prompt: string | undefined) => void; +} + +export const NavigationWarningPromptContext = createContext({ + setPrompt: (prompt: string | undefined) => {}, +}); + +export const useNavigationWarningPrompt = () => { + return useContext(NavigationWarningPromptContext); +}; + +export const NavigationWarningPromptProvider: React.FC = ({ children }) => { + const [prompt, setPrompt] = useState(undefined); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts new file mode 100644 index 0000000000000..dcdbf8e912a83 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './context'; +export * from './prompt'; diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx new file mode 100644 index 0000000000000..65ec4729c036d --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx @@ -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 React, { useEffect } from 'react'; +import { useNavigationWarningPrompt } from './context'; + +interface Props { + prompt?: string; +} + +export const Prompt: React.FC = ({ prompt }) => { + const { setPrompt } = useNavigationWarningPrompt(); + + useEffect(() => { + setPrompt(prompt); + return () => { + setPrompt(undefined); + }; + }, [prompt, setPrompt]); + + return null; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 000d0823311b3..a52659dae01f1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -149,6 +149,14 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID)).toBe(undefined); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); + test('alerts as expected with the outside range comparator', async () => { + await execute(Comparator.OUTSIDE_RANGE, [0, 0.75]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute(Comparator.OUTSIDE_RANGE, [0, 1.5]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); test('reports expected values to the action context', async () => { await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index c5ea65f7a4d1a..946f1c14bf593 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -11,7 +11,7 @@ import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_ty import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; -import { MetricExpressionParams, Comparator, AlertStates } from './types'; +import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './types'; import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; @@ -39,7 +39,7 @@ const getCurrentValueFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state const mostRecentBucket = buckets[buckets.length - 1]; - if (aggType === 'count') { + if (aggType === Aggregators.COUNT) { return mostRecentBucket.doc_count; } const { value } = mostRecentBucket.aggregatedValue; @@ -70,10 +70,10 @@ export const getElasticsearchMetricQuery = ( groupBy?: string, filterQuery?: string ) => { - if (aggType === 'count' && metric) { + if (aggType === Aggregators.COUNT && metric) { throw new Error('Cannot aggregate document count with a metric'); } - if (aggType !== 'count' && !metric) { + if (aggType !== Aggregators.COUNT && !metric) { throw new Error('Can only aggregate without a metric if using the document count aggregator'); } const interval = `${timeSize}${timeUnit}`; @@ -85,9 +85,9 @@ export const getElasticsearchMetricQuery = ( const offset = getDateHistogramOffset(from, interval); const aggregations = - aggType === 'count' + aggType === Aggregators.COUNT ? {} - : aggType === 'rate' + : aggType === Aggregators.RATE ? networkTraffic('aggregatedValue', metric) : { aggregatedValue: { @@ -242,7 +242,8 @@ const getMetric: ( const comparatorMap = { [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => value >= Math.min(a, b) && value <= Math.max(a, b), - // `threshold` is always an array of numbers in case the BETWEEN comparator is + [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b, + // `threshold` is always an array of numbers in case the BETWEEN/OUTSIDE_RANGE comparator is // used; all other compartors will just destructure the first value in the array [Comparator.GT]: (a: number, [b]: number[]) => a > b, [Comparator.LT]: (a: number, [b]: number[]) => a < b, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 8808219cabaa7..b697af4fa4c3b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -7,8 +7,15 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { PluginSetupContract } from '../../../../../alerting/server'; +import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; + +const oneOfLiterals = (arrayOfLiterals: Readonly) => + schema.string({ + validate: value => + arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, + }); export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { if (!alertingPlugin) { @@ -20,13 +27,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet const baseCriterion = { threshold: schema.arrayOf(schema.number()), - comparator: schema.oneOf([ - schema.literal('>'), - schema.literal('<'), - schema.literal('>='), - schema.literal('<='), - schema.literal('between'), - ]), + comparator: oneOfLiterals(Object.values(Comparator)), timeUnit: schema.string(), timeSize: schema.number(), }; @@ -34,13 +35,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet const nonCountCriterion = schema.object({ ...baseCriterion, metric: schema.string(), - aggType: schema.oneOf([ - schema.literal('avg'), - schema.literal('min'), - schema.literal('max'), - schema.literal('rate'), - schema.literal('cardinality'), - ]), + aggType: oneOfLiterals(METRIC_EXPLORER_AGGREGATIONS), }); const countCriterion = schema.object({ diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index abed691f109c0..18f5503fe2c9e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerAggregation } from '../../../../common/http_api/metrics_explorer'; - export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export enum Comparator { @@ -14,6 +12,17 @@ export enum Comparator { GT_OR_EQ = '>=', LT_OR_EQ = '<=', BETWEEN = 'between', + OUTSIDE_RANGE = 'outside', +} + +export enum Aggregators { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', } export enum AlertStates { @@ -34,7 +43,7 @@ interface BaseMetricExpressionParams { } interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { - aggType: Exclude; + aggType: Exclude; metric: string; } diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index 07acdf8affd49..9cd4821c2a727 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -1,9 +1,9 @@ # Ingest Manager ## Plugin - - No features enabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/feature-ingest/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L19) - - Setting `xpack.ingestManager.enabled=true` is required to enable the plugin. It adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) - - Adding `--xpack.ingestManager.epm.enabled=true` will add the EPM API & UI - - Adding `--xpack.ingestManager.fleet.enabled=true` will add the Fleet API & UI + - The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) + - Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) + - Adding `--xpack.ingestManager.epm.enabled=false` will disable the EPM API & UI + - Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) - [Integration tests](server/integration_tests/router.test.ts) - Both EPM and Fleet require `ingestManager` be enabled. They are not standalone features. @@ -25,7 +25,7 @@ One common development workflow is: ``` - Start Kibana in another shell ``` - yarn start --xpack.ingestManager.enabled=true --xpack.ingestManager.epm.enabled=true --xpack.ingestManager.fleet.enabled=true --no-base-path --xpack.endpoint.enabled=true + yarn start --xpack.ingestManager.enabled=true --no-base-path --xpack.endpoint.enabled=true ``` This plugin follows the `common`, `server`, `public` structure from the [Architecture Style Guide diff --git a/x-pack/plugins/ingest_manager/common/constants/output.ts b/x-pack/plugins/ingest_manager/common/constants/output.ts index 6060a2b63fc8e..4c22d0e3fe7a3 100644 --- a/x-pack/plugins/ingest_manager/common/constants/output.ts +++ b/x-pack/plugins/ingest_manager/common/constants/output.ts @@ -12,5 +12,4 @@ export const DEFAULT_OUTPUT = { is_default: true, type: OutputType.Elasticsearch, hosts: [''], - api_key: '', }; diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index a31d38a723c2c..98ca52651a2ae 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -5,9 +5,10 @@ */ // Base API paths export const API_ROOT = `/api/ingest_manager`; +export const EPM_API_ROOT = `${API_ROOT}/epm`; +export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`; export const DATASOURCE_API_ROOT = `${API_ROOT}/datasources`; export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; -export const EPM_API_ROOT = `${API_ROOT}/epm`; export const FLEET_API_ROOT = `${API_ROOT}/fleet`; // EPM API routes @@ -23,6 +24,11 @@ export const EPM_API_ROUTES = { CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, }; +// Data stream API routes +export const DATA_STREAM_API_ROUTES = { + LIST_PATTERN: `${DATA_STREAM_API_ROOT}`, +}; + // Datasource API routes export const DATASOURCE_API_ROUTES = { LIST_PATTERN: `${DATASOURCE_API_ROOT}`, @@ -53,7 +59,8 @@ export const AGENT_API_ROUTES = { ACKS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/acks`, ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`, ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`, - UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/unenroll`, + UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/unenroll`, + REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`, STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`, }; diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index 3496ea782ee99..17509571f1985 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { NewDatasource, DatasourceInput } from '../types'; +import { Datasource, NewDatasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { - const mockDatasource: NewDatasource = { + const mockNewDatasource: NewDatasource = { name: 'mock-datasource', description: '', config_id: '', @@ -17,6 +17,12 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { inputs: [], }; + const mockDatasource: Datasource = { + ...mockNewDatasource, + id: 'some-uuid', + revision: 1, + }; + const mockInput: DatasourceInput = { type: 'test-logs', enabled: true, @@ -70,7 +76,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { it('returns agent datasource config for datasource with no inputs', () => { expect(storedDatasourceToAgentDatasource(mockDatasource)).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -87,7 +94,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }, }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -99,9 +107,21 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); + it('uses name for id when id is not provided in case of new datasource', () => { + expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({ + id: 'mock-datasource', + name: 'mock-datasource', + namespace: 'default', + enabled: true, + use_output: 'default', + inputs: [], + }); + }); + it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -140,7 +160,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { ], }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -169,7 +190,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { inputs: [{ ...mockInput, enabled: false }], }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index b509878b7f945..9e09d3fa3153a 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -12,7 +12,8 @@ export const storedDatasourceToAgentDatasource = ( const { name, namespace, enabled, package: pkg, inputs } = datasource; const fullDatasource: FullAgentConfigDatasource = { - id: name, + id: 'id' in datasource ? datasource.id : name, + name, namespace, enabled, use_output: DEFAULT_OUTPUT.name, // TODO: hardcoded to default output for now diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 7cc6fc3c66afb..46b76d886f3cd 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -8,6 +8,7 @@ import { EPM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_CONFIG_API_ROUTES, + DATA_STREAM_API_ROUTES, FLEET_SETUP_API_ROUTES, AGENT_API_ROUTES, ENROLLMENT_API_KEY_ROUTES, @@ -88,6 +89,12 @@ export const agentConfigRouteService = { }, }; +export const dataStreamRouteService = { + getListPath: () => { + return DATA_STREAM_API_ROUTES.LIST_PATTERN; + }, +}; + export const fleetSetupRouteService = { getFleetSetupPath: () => FLEET_SETUP_API_ROUTES.INFO_PATTERN, postFleetSetupPath: () => FLEET_SETUP_API_ROUTES.CREATE_PATTERN, @@ -97,7 +104,10 @@ export const agentRouteService = { getInfoPath: (agentId: string) => AGENT_API_ROUTES.INFO_PATTERN.replace('{agentId}', agentId), getUpdatePath: (agentId: string) => AGENT_API_ROUTES.UPDATE_PATTERN.replace('{agentId}', agentId), getEventsPath: (agentId: string) => AGENT_API_ROUTES.EVENTS_PATTERN.replace('{agentId}', agentId), - getUnenrollPath: () => AGENT_API_ROUTES.UNENROLL_PATTERN, + getUnenrollPath: (agentId: string) => + AGENT_API_ROUTES.UNENROLL_PATTERN.replace('{agentId}', agentId), + getReassignPath: (agentId: string) => + AGENT_API_ROUTES.REASSIGN_PATTERN.replace('{agentId}', agentId), getListPath: () => AGENT_API_ROUTES.LIST_PATTERN, getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN, }; diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index 4d03a30f9a590..fcd3955f3a32f 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -20,15 +20,18 @@ export interface NewAgentAction { sent_at?: string; } -export type AgentAction = NewAgentAction & { +export interface AgentAction extends NewAgentAction { id: string; agent_id: string; created_at: string; -} & SavedObjectAttributes; +} -export interface AgentActionSOAttributes extends NewAgentAction, SavedObjectAttributes { +export interface AgentActionSOAttributes extends SavedObjectAttributes { + type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE'; + sent_at?: string; created_at: string; agent_id: string; + data?: string; } export interface AgentEvent { @@ -64,8 +67,9 @@ interface AgentBase { shared_id?: string; access_api_key_id?: string; default_api_key?: string; + default_api_key_id?: string; config_id?: string; - config_revision?: number; + config_revision?: number | null; config_newest_revision?: number; last_checkin?: string; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 002c3784446a8..2372caee512af 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -34,8 +34,10 @@ export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { revision: number; } -export type FullAgentConfigDatasource = Pick & { - id: string; +export type FullAgentConfigDatasource = Pick< + Datasource, + 'id' | 'name' | 'namespace' | 'enabled' +> & { package?: Pick; use_output: string; inputs: Array< diff --git a/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts new file mode 100644 index 0000000000000..7da9bbad1b170 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts @@ -0,0 +1,15 @@ +/* + * 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 DataStream { + index: string; + dataset: string; + namespace: string; + type: string; + package: string; + last_activity: string; + size_in_bytes: number; +} diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 53ad0310ea613..c750aa99204fa 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -57,6 +57,7 @@ export interface RegistryPackage { icons?: RegistryImage[]; assets?: string[]; internal?: boolean; + removable?: boolean; format_version: string; datasets?: Dataset[]; datasources?: RegistryDatasource[]; @@ -256,6 +257,10 @@ export enum DefaultPackages { endpoint = 'endpoint', } +export interface IndexTemplateMappings { + properties: any; +} + export interface IndexTemplate { order: number; index_patterns: string[]; @@ -263,3 +268,8 @@ export interface IndexTemplate { mappings: object; aliases: object; } + +export interface TemplateRef { + templateName: string; + indexTemplate: IndexTemplate; +} diff --git a/x-pack/plugins/ingest_manager/common/types/models/index.ts b/x-pack/plugins/ingest_manager/common/types/models/index.ts index 579b510e52daa..f73ab7af636a9 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/index.ts @@ -7,6 +7,7 @@ export * from './agent'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './output'; export * from './epm'; export * from './enrollment_api_key'; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 21ab41740ce3e..64ed95db74f4c 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -96,16 +96,23 @@ export interface PostNewAgentActionResponse { } export interface PostAgentUnenrollRequest { - body: { kuery: string } | { ids: string[] }; + params: { + agentId: string; + }; } export interface PostAgentUnenrollResponse { - results: Array<{ - success: boolean; - error?: any; - id: string; - action: string; - }>; + success: boolean; +} + +export interface PutAgentReassignRequest { + params: { + agentId: string; + }; + body: { config_id: string }; +} + +export interface PutAgentReassignResponse { success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts new file mode 100644 index 0000000000000..24f8110562bfc --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts @@ -0,0 +1,10 @@ +/* + * 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 { DataStream } from '../models'; + +export interface GetDataStreamsResponse { + data_streams: DataStream[]; +} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts index dc1d748a8743a..c4ba8ee595acf 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts @@ -4,16 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface GetFleetSetupRequest {} - -export interface CreateFleetSetupRequest { - body: { - fleet_enroll_username: string; - fleet_enroll_password: string; - }; -} - export interface CreateFleetSetupResponse { isInitialized: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts index abe1bc8e3eddb..c1805023f497a 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts @@ -5,6 +5,7 @@ */ export * from './common'; export * from './datasource'; +export * from './data_stream'; export * from './agent'; export * from './agent_config'; export * from './fleet_setup'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx index 1aab6d901a992..ceb87fb048ae3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx @@ -31,7 +31,7 @@ const Tabs = styled(EuiTabs)` `; export interface HeaderProps { - restrictHeaderWidth?: number; + maxWidth?: number; leftColumn?: JSX.Element; rightColumn?: JSX.Element; rightColumnGrow?: EuiFlexItemProps['grow']; @@ -52,10 +52,10 @@ export const Header: React.FC = ({ rightColumn, rightColumnGrow, tabs, - restrictHeaderWidth, + maxWidth, }) => ( - + ({ + method: 'put', + path: agentRouteService.getReassignPath(agentId), + body, + ...options, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts new file mode 100644 index 0000000000000..9acf4b1e17449 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts @@ -0,0 +1,15 @@ +/* + * 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 { useRequest } from './use_request'; +import { dataStreamRouteService } from '../../services'; +import { GetDataStreamsResponse } from '../../types'; + +export const useGetDataStreams = () => { + return useRequest({ + path: dataStreamRouteService.getListPath(), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts index 5014049407e65..084aba9a34309 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -6,6 +6,7 @@ export { setHttpClient, sendRequest, useRequest } from './use_request'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './agents'; export * from './enrollment_api_keys'; export * from './epm'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index f7c2805c6ea7c..6485862830d8a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -16,10 +16,10 @@ import { IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from './constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; -import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp } from './sections'; +import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { sendSetup } from './hooks/use_request/setup'; @@ -98,6 +98,11 @@ const IngestManagerRoutes = ({ ...rest }) => { + + + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index f797c509bfca0..f1f9063de72f0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; import { AlphaMessaging } from '../components'; import { useLink, useConfig } from '../hooks'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; interface Props { section?: Section; @@ -20,8 +20,6 @@ interface Props { const Container = styled.div` min-height: calc(100vh - ${props => props.theme.eui.euiHeaderChildSize}); background: ${props => props.theme.eui.euiColorEmptyShade}; - display: flex; - flex-direction: column; `; const Nav = styled.nav` @@ -78,6 +76,12 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre defaultMessage="Fleet" /> + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx index bb867718204b2..d5ce5e17ad84e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx @@ -10,18 +10,22 @@ import { Header, HeaderProps } from '../components'; const Page = styled(EuiPage)` background: ${props => props.theme.eui.euiColorEmptyShade}; - flex: 1; - align-items: flex-start; `; interface Props extends HeaderProps { restrictWidth?: number; + restrictHeaderWidth?: number; children?: React.ReactNode; } -export const WithHeaderLayout: React.FC = ({ restrictWidth, children, ...rest }) => ( +export const WithHeaderLayout: React.FC = ({ + restrictWidth, + restrictHeaderWidth, + children, + ...rest +}) => ( -
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx index 9f9fa03942c09..cad98c5a0a7e1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/without_header.tsx @@ -9,8 +9,6 @@ import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; const Page = styled(EuiPage)` background: ${props => props.theme.eui.euiColorEmptyShade}; - flex: 1; - align-items: flex-start; `; interface Props { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 73a7ba8ec119d..39d882f7fdf65 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -18,16 +18,14 @@ import { import { WithHeaderLayout } from '../../../../layouts'; import { AgentConfig, PackageInfo } from '../../../../types'; import { PackageIcon } from '../../../../components/package_icon'; -import { CreateDatasourceFrom, CreateDatasourceStep } from '../types'; +import { CreateDatasourceFrom } from '../types'; export const CreateDatasourcePageLayout: React.FunctionComponent<{ from: CreateDatasourceFrom; - basePath: string; cancelUrl: string; - maxStep: CreateDatasourceStep | ''; agentConfig?: AgentConfig; packageInfo?: PackageInfo; -}> = ({ from, basePath, cancelUrl, maxStep, agentConfig, packageInfo, children }) => { +}> = ({ from, cancelUrl, agentConfig, packageInfo, children }) => { const leftColumn = ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 1ad579d591b21..3c79fe17fdbb5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -28,12 +28,11 @@ import { } from '../../../hooks'; import { useLinks as useEPMLinks } from '../../epm/hooks'; import { CreateDatasourcePageLayout, ConfirmCreateDatasourceModal } from './components'; -import { CreateDatasourceFrom, CreateDatasourceStep } from './types'; +import { CreateDatasourceFrom } from './types'; import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services'; import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; import { StepConfigureDatasource } from './step_configure_datasource'; - import { StepDefineDatasource } from './step_define_datasource'; export const CreateDatasourcePage: React.FunctionComponent = () => { @@ -43,11 +42,9 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } = useConfig(); const { params: { configId, pkgkey }, - url: basePath, } = useRouteMatch(); const history = useHistory(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; - const [maxStep, setMaxStep] = useState(''); // Agent config and package info states const [agentConfig, setAgentConfig] = useState(); @@ -91,7 +88,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } else { setFormState('INVALID'); setPackageInfo(undefined); - setMaxStep(''); } // eslint-disable-next-line no-console @@ -105,7 +101,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } else { setFormState('INVALID'); setAgentConfig(undefined); - setMaxStep(''); } // eslint-disable-next-line no-console @@ -189,9 +184,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const layoutProps = { from, - basePath, cancelUrl, - maxStep, agentConfig, packageInfo, }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts index bd05be2d8a558..85cc758fc4c46 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts @@ -5,4 +5,3 @@ */ export type CreateDatasourceFrom = 'package' | 'config'; -export type CreateDatasourceStep = 'selectConfig' | 'selectPackage' | 'configure' | 'review'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index 56b109a9bc062..ad27c590d5eaa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx @@ -27,7 +27,9 @@ import { Loading } from '../../../../../components'; const CONFIG_KEYS_ORDER = [ 'id', + 'name', 'revision', + 'type', 'outputs', 'datasources', 'enabled', @@ -52,7 +54,7 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { return ( - + {dump(fullConfigRequest.data.item, { sortKeys: (keyA: string, keyB: string) => { const indexA = CONFIG_KEYS_ORDER.indexOf(keyA); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 0498e814440c7..1ea162252c741 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -191,7 +191,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Name', }), width: '20%', - // FIXME: use version once available - see: https://github.com/elastic/kibana/issues/56750 render: (name: string, agentConfig: AgentConfig) => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx new file mode 100644 index 0000000000000..7b0641e66fd43 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { DataStreamListPage } from './list_page'; + +export const DataStreamApp: React.FunctionComponent = () => { + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx new file mode 100644 index 0000000000000..d7a3e933f3bb5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -0,0 +1,283 @@ +/* + * 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, { useMemo } from 'react'; +import { + EuiBadge, + EuiButton, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiInMemoryTable, + EuiTableActionsColumnType, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; +import { DataStream } from '../../../types'; +import { WithHeaderLayout } from '../../../layouts'; +import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; + +const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( + + + +

+ +

+
+
+ + +

+ +

+
+
+
+ } + > + {children} + +); + +export const DataStreamListPage: React.FunctionComponent<{}> = () => { + const { + data: { fieldFormats }, + } = useStartDeps(); + + const { pagination, pageSizeOptions } = usePagination(); + + // Fetch agent configs + const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams(); + + // Some configs retrieved, set up table props + const columns = useMemo(() => { + const cols: Array< + EuiTableFieldDataColumnType | EuiTableActionsColumnType + > = [ + { + field: 'dataset', + sortable: true, + width: '25%', + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + }, + { + field: 'type', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + }, + { + field: 'namespace', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + render: (namespace: string) => { + return namespace ? {namespace} : ''; + }, + }, + { + field: 'package', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + }, + { + field: 'last_activity', + sortable: true, + width: '25%', + dataType: 'date', + name: i18n.translate('xpack.ingestManager.dataStreamList.lastActivityColumnTitle', { + defaultMessage: 'Last activity', + }), + render: (date: DataStream['last_activity']) => { + try { + const formatter = fieldFormats.getInstance('date'); + return formatter.convert(date); + } catch (e) { + return ; + } + }, + }, + { + field: 'size_in_bytes', + sortable: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.sizeColumnTitle', { + defaultMessage: 'Size', + }), + render: (size: DataStream['size_in_bytes']) => { + try { + const formatter = fieldFormats.getInstance('bytes'); + return formatter.convert(size); + } catch (e) { + return `${size}b`; + } + }, + }, + ]; + return cols; + }, [fieldFormats]); + + const emptyPrompt = useMemo( + () => ( + + + + } + /> + ), + [] + ); + + const filterOptions: { [key: string]: string[] } = { + dataset: [], + type: [], + namespace: [], + package: [], + }; + + if (dataStreamsData && dataStreamsData.data_streams.length) { + dataStreamsData.data_streams.forEach(stream => { + const { dataset, type, namespace, package: pkg } = stream; + if (!filterOptions.dataset.includes(dataset)) { + filterOptions.dataset.push(dataset); + } + if (!filterOptions.type.includes(type)) { + filterOptions.type.push(type); + } + if (!filterOptions.namespace.includes(namespace)) { + filterOptions.namespace.push(namespace); + } + if (!filterOptions.package.includes(pkg)) { + filterOptions.package.push(pkg); + } + }); + } + + return ( + + + ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( + emptyPrompt + ) : ( + + ) + } + items={dataStreamsData ? dataStreamsData.data_streams : []} + itemId="index" + columns={columns} + pagination={{ + initialPageSize: pagination.pageSize, + pageSizeOptions, + }} + sorting={true} + search={{ + toolsRight: [ + sendRequest()}> + + , + ], + box: { + placeholder: i18n.translate( + 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', + { + defaultMessage: 'Filter data streams', + } + ), + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'dataset', + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + multiSelect: 'or', + options: filterOptions.dataset.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: filterOptions.type.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'namespace', + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + multiSelect: 'or', + options: filterOptions.namespace.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'package', + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + multiSelect: 'or', + options: filterOptions.package.map(option => ({ + value: option, + name: option, + })), + }, + ], + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index 0d4b395895322..a3d24e7806f34 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -50,10 +50,18 @@ export function Content(props: ContentProps) { type ContentPanelProps = PackageInfo & Pick; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title } = props; + const { panel, name, version, assets, title, removable } = props; switch (panel) { case 'settings': - return ; + return ( + + ); case 'data-sources': return ; case 'overview': diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index ff7ecf97714b6..f947466caf4b0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -13,8 +13,14 @@ import { InstallStatus, PackageInfo } from '../../../../types'; import { InstallationButton } from './installation_button'; import { useGetDatasources } from '../../../../hooks'; +const NoteLabel = () => ( + +); export const SettingsPanel = ( - props: Pick + props: Pick ) => { const getPackageInstallStatus = useGetPackageInstallStatus(); const { data: datasourcesData } = useGetDatasources({ @@ -22,10 +28,9 @@ export const SettingsPanel = ( page: 1, kuery: `datasources.package.name:${props.name}`, }); - const { name, title } = props; + const { name, title, removable } = props; const packageInstallStatus = getPackageInstallStatus(name); const packageHasDatasources = !!datasourcesData?.total; - return ( @@ -89,12 +94,12 @@ export const SettingsPanel = (

- {packageHasDatasources && ( + {packageHasDatasources && removable === true && (

- + + + ), + }} + /> +

+ )} + {removable === false && ( +

+ + ), }} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx index 0844368dc214b..653e2eb9a3a3b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -13,10 +13,13 @@ import { EuiFlexItem, EuiDescriptionList, EuiButton, + EuiPopover, EuiDescriptionListTitle, EuiDescriptionListDescription, EuiButtonEmpty, EuiIconTip, + EuiContextMenuPanel, + EuiContextMenuItem, EuiTextColor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -26,7 +29,7 @@ import { Agent } from '../../../../types'; import { AgentHealth } from '../../components/agent_health'; import { useCapabilities, useGetOneAgentConfig } from '../../../../hooks'; import { Loading } from '../../../../components'; -import { ConnectedLink } from '../../components'; +import { ConnectedLink, AgentReassignConfigFlyout } from '../../components'; import { AgentUnenrollProvider } from '../../components/agent_unenroll_provider'; const Item: React.FunctionComponent<{ label: string }> = ({ label, children }) => { @@ -56,6 +59,15 @@ export const AgentDetailSection: React.FunctionComponent = ({ agent }) => const hasWriteCapabilites = useCapabilities().write; const metadataFlyout = useFlyout(); const refreshAgent = useAgentRefresh(); + // Actions menu + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsActionsPopoverOpen(false), [ + setIsActionsPopoverOpen, + ]); + const handleToggleMenu = useCallback(() => setIsActionsPopoverOpen(!isActionsPopoverOpen), [ + isActionsPopoverOpen, + ]); + const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); // Fetch AgentConfig information const { isLoading: isAgentConfigLoading, data: agentConfigData } = useGetOneAgentConfig( @@ -111,6 +123,9 @@ export const AgentDetailSection: React.FunctionComponent = ({ agent }) => return ( <> + {isReassignFlyoutOpen && ( + setIsReassignFlyoutOpen(false)} /> + )} @@ -123,21 +138,55 @@ export const AgentDetailSection: React.FunctionComponent = ({ agent }) => - - {unenrollAgentsPrompt => ( - { - unenrollAgentsPrompt([agent.id], 1, refreshAgent); - }} - > + - )} - + } + isOpen={isActionsPopoverOpen} + closePopover={handleCloseMenu} + > + { + handleCloseMenu(); + setIsReassignFlyoutOpen(true); + }} + key="reassignConfig" + > + + , + + + {unenrollAgentsPrompt => ( + { + unenrollAgentsPrompt([agent.id], 1, refreshAgent); + }} + > + + + )} + , + ]} + /> + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index d363c472f2305..c79255104a030 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -35,7 +35,7 @@ import { useUrlParams, useLink, } from '../../../hooks'; -import { ConnectedLink } from '../components'; +import { ConnectedLink, AgentReassignConfigFlyout } from '../components'; import { SearchBar } from '../../../components/search_bar'; import { AgentHealth } from '../components/agent_health'; import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; @@ -71,61 +71,76 @@ const statusFilters = [ }, ] as Array<{ label: string; status: string }>; -const RowActions = React.memo<{ agent: Agent; refresh: () => void }>(({ agent, refresh }) => { - const hasWriteCapabilites = useCapabilities().write; - const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); - const [isOpen, setIsOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); - const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); +const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( + ({ agent, refresh, onReassignClick }) => { + const hasWriteCapabilites = useCapabilities().write; + const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); - return ( - - } - isOpen={isOpen} - closePopover={handleCloseMenu} - > - - - , + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + , + { + handleCloseMenu(); + onReassignClick(); + }} + key="reassignConfig" + > + + , - - {unenrollAgentsPrompt => ( - { - unenrollAgentsPrompt([agent.id], 1, () => { - refresh(); - }); - }} - > - - - )} - , - ]} - /> - - ); -}); + + {unenrollAgentsPrompt => ( + { + unenrollAgentsPrompt([agent.id], 1, () => { + refresh(); + }); + }} + > + + + )} + , + ]} + /> + + ); + } +); export const AgentListPage: React.FunctionComponent<{}> = () => { const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; @@ -136,8 +151,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Table and search states const [search, setSearch] = useState(defaultKuery); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgents, setSelectedAgents] = useState([]); - const [areAllAgentsSelected, setAreAllAgentsSelected] = useState(false); // Configs state (for filtering) const [isConfigsFilterOpen, setIsConfigsFilterOpen] = useState(false); @@ -159,6 +172,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); + // Agent reassignment flyout state + const [agentToReassignId, setAgentToReassignId] = useState(undefined); + let kuery = search.trim(); if (selectedConfigs.length) { if (kuery) { @@ -227,47 +243,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {host} ), - footer: () => { - if (selectedAgents.length === agents.length && totalAgents > selectedAgents.length) { - return areAllAgentsSelected ? ( - setAreAllAgentsSelected(false)}> - - - ), - }} - /> - ) : ( - setAreAllAgentsSelected(true)}> - - - ), - }} - /> - ); - } - return null; - }, }, { field: 'active', @@ -350,7 +325,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { actions: [ { render: (agent: Agent) => { - return agentsRequest.sendRequest()} />; + return ( + agentsRequest.sendRequest()} + onReassignClick={() => setAgentToReassignId(agent.id)} + /> + ); }, }, ], @@ -381,6 +362,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { /> ); + const agentToReassign = agentToReassignId && agents.find(a => a.id === agentToReassignId); + return ( <> {isEnrollmentFlyoutOpen ? ( @@ -389,48 +372,16 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { onClose={() => setIsEnrollmentFlyoutOpen(false)} /> ) : null} + {agentToReassign && ( + { + setAgentToReassignId(undefined); + agentsRequest.sendRequest(); + }} + /> + )} - {selectedAgents.length ? ( - - - {unenrollAgentsPrompt => ( - { - unenrollAgentsPrompt( - areAllAgentsSelected ? search : selectedAgents.map(agent => agent.id), - areAllAgentsSelected ? totalAgents : selectedAgents.length, - () => { - // Reload agents if on first page and no search query, otherwise - // reset to first page and reset search, which will trigger a reload - if (pagination.currentPage === 1 && !search) { - agentsRequest.sendRequest(); - } else { - setPagination({ - ...pagination, - currentPage: 1, - }); - setSearch(''); - } - - setAreAllAgentsSelected(false); - setSelectedAgents([]); - } - ); - }} - > - - - )} - - - ) : null} @@ -575,14 +526,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { items={totalAgents ? agents : []} itemId="id" columns={columns} - isSelectable={true} - selection={{ - selectable: (agent: Agent) => agent.active, - onSelectionChange: (newSelectedAgents: Agent[]) => { - setSelectedAgents(newSelectedAgents); - setAreAllAgentsSelected(false); - }, - }} pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx new file mode 100644 index 0000000000000..11a049047b787 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx @@ -0,0 +1,186 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiFlyoutFooter, + EuiSelect, + EuiFormRow, + EuiText, + EuiBadge, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Datasource, Agent } from '../../../../types'; +import { + useGetOneAgentConfig, + sendPutAgentReassign, + useCore, + useGetAgentConfigs, +} from '../../../../hooks'; +import { PackageIcon } from '../../../../components/package_icon'; + +interface Props { + onClose: () => void; + agent: Agent; +} + +export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onClose, agent }) => { + const { notifications } = useCore(); + const [selectedAgentConfigId, setSelectedAgentConfigId] = useState( + agent.config_id + ); + + const agentConfigsRequest = useGetAgentConfigs(); + const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; + + const agentConfigRequest = useGetOneAgentConfig(selectedAgentConfigId as string); + const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; + + const [isSubmitting, setIsSubmitting] = useState(false); + + async function onSubmit() { + try { + setIsSubmitting(true); + if (!selectedAgentConfigId) { + throw new Error('No selected config id'); + } + const res = await sendPutAgentReassign(agent.id, { + config_id: selectedAgentConfigId, + }); + if (res.error) { + throw res.error; + } + setIsSubmitting(false); + const successMessage = i18n.translate( + 'xpack.ingestManager.agentReassignConfig.successSingleNotificationTitle', + { + defaultMessage: 'Agent configuration reassigned', + } + ); + notifications.toasts.addSuccess(successMessage); + onClose(); + } catch (error) { + setIsSubmitting(false); + notifications.toasts.addError(error, { + title: 'Unable to reassign agent configuration', + }); + } + } + + return ( + + + +

+ +

+ + + + + + + + + + + ({ + value: config.id, + text: config.name, + }))} + value={selectedAgentConfigId} + onChange={e => setSelectedAgentConfigId(e.target.value)} + /> + + + + + + {agentConfig && ( + + {agentConfig.datasources.length}, + }} + /> + + )} + + {agentConfig && + (agentConfig.datasources as Datasource[]).map((datasource, idx) => { + if (!datasource.package) { + return null; + } + return ( + + + + + + {datasource.package.title} + + + ); + })} + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx index 25499495a7897..fec2253c0dd56 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx @@ -39,7 +39,8 @@ export const AgentUnenrollProvider: React.FunctionComponent = ({ children ) => { if ( agentsToUnenroll === undefined || - (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length === 0) + // !Only supports unenrolling one agent + (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length !== 1) ) { throw new Error('No agents specified for unenrollment'); } @@ -60,55 +61,27 @@ export const AgentUnenrollProvider: React.FunctionComponent = ({ children setIsLoading(true); try { - const unenrollByKuery = typeof agents === 'string'; - const { data, error } = await sendRequest({ - path: agentRouteService.getUnenrollPath(), + const agentId = agents[0]; + const { error } = await sendRequest({ + path: agentRouteService.getUnenrollPath(agentId), method: 'post', - body: JSON.stringify({ - kuery: unenrollByKuery ? agents : undefined, - ids: !unenrollByKuery ? agents : undefined, - }), }); if (error) { throw new Error(error.message); } - const results = data ? data.results : []; - - const successfulResults = results.filter(result => result.success); - const failedResults = results.filter(result => !result.success); - - if (successfulResults.length) { - const hasMultipleSuccesses = successfulResults.length > 1; - const successMessage = hasMultipleSuccesses - ? i18n.translate('xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle', { - defaultMessage: 'Unenrolled {count} agents', - values: { count: successfulResults.length }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { - defaultMessage: "Unenrolled agent '{id}'", - values: { id: successfulResults[0].id }, - }); - core.notifications.toasts.addSuccess(successMessage); - } - - if (failedResults.length) { - const hasMultipleFailures = failedResults.length > 1; - const failureMessage = hasMultipleFailures - ? i18n.translate('xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle', { - defaultMessage: 'Error unenrolling {count} agents', - values: { count: failedResults.length }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle', { - defaultMessage: "Error unenrolling agent '{id}'", - values: { id: failedResults[0].id }, - }); - core.notifications.toasts.addDanger(failureMessage); - } + const successMessage = i18n.translate( + 'xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', + { + defaultMessage: "Unenrolled agent '{id}'", + values: { id: agentId }, + } + ); + core.notifications.toasts.addSuccess(successMessage); if (onSuccessCallback.current) { - onSuccessCallback.current(successfulResults.map(result => result.id)); + onSuccessCallback.current([agentId]); } } catch (e) { core.notifications.toasts.addDanger( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx index 19378fe2fb952..a0092f4073e5a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx @@ -5,5 +5,6 @@ */ export * from './loading'; +export * from './agent_reassign_config_flyout'; export * from './navigation/child_routes'; export * from './navigation/connected_link'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx index c691bb609d435..1f46c4cc820cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx @@ -6,6 +6,7 @@ export { IngestManagerOverview } from './overview'; export { EPMApp } from './epm'; export { AgentConfigApp } from './agent_config'; +export { DataStreamApp } from './data_stream'; export { FleetApp } from './fleet'; -export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet'; +export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet' | 'data_stream'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 5ebd1300baf65..53dbe295718c5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -9,6 +9,7 @@ export { getFlattenedObject } from '../../../../../../../src/core/utils'; export { agentConfigRouteService, datasourceRouteService, + dataStreamRouteService, fleetSetupRouteService, agentRouteService, enrollmentAPIKeyRouteService, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 32615278b67d7..8ca1495a94071 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -17,6 +17,7 @@ export { DatasourceInput, DatasourceInputStream, DatasourceConfigRecordEntry, + DataStream, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, @@ -30,6 +31,8 @@ export { // API schemas - Datasource CreateDatasourceRequest, CreateDatasourceResponse, + // API schemas - Data Streams + GetDataStreamsResponse, // API schemas - Agents GetAgentsResponse, GetAgentsRequest, @@ -39,6 +42,8 @@ export { GetOneAgentEventsResponse, GetAgentStatusRequest, GetAgentStatusResponse, + PutAgentReassignRequest, + PutAgentReassignResponse, // API schemas - Enrollment API Keys GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index 6ac92ca5d2a91..b2e72fefe5997 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -12,6 +12,7 @@ export { // Routes PLUGIN_ID, EPM_API_ROUTES, + DATA_STREAM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_API_ROUTES, AGENT_CONFIG_API_ROUTES, diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 7859c44ccfd89..851a58f5adac2 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -6,9 +6,12 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; - -export { ESIndexPatternService } from './services'; -export { IngestManagerSetupContract } from './plugin'; +export { AgentService, ESIndexPatternService } from './services'; +export { + IngestManagerSetupContract, + IngestManagerSetupDeps, + IngestManagerStartContract, +} from './plugin'; export const config = { exposeToBrowser: { @@ -18,11 +21,11 @@ export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: false }), epm: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), registryUrl: schema.uri({ defaultValue: 'https://epr-staging.elastic.co' }), }), fleet: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), kibana: schema.object({ host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 4dd070a7414f0..55aea4b1a4cdd 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,11 +11,12 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, - RecursiveReadonly, } from 'kibana/server'; -import { deepFreeze } from '../../../../src/core/utils'; import { LicensingPluginSetup } from '../../licensing/server'; -import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server'; +import { + EncryptedSavedObjectsPluginStart, + EncryptedSavedObjectsPluginSetup, +} from '../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { @@ -28,10 +29,11 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, } from './constants'; - +import { registerEncryptedSavedObjects } from './saved_objects'; import { registerEPMRoutes, registerDatasourceRoutes, + registerDataStreamRoutes, registerAgentConfigRoutes, registerSetupRoutes, registerAgentRoutes, @@ -40,25 +42,19 @@ import { } from './routes'; import { IngestManagerConfigType } from '../common'; -import { - appContextService, - ESIndexPatternService, - ESIndexPatternSavedObjectService, -} from './services'; - -/** - * Describes public IngestManager plugin contract returned at the `setup` stage. - */ -export interface IngestManagerSetupContract { - esIndexPatternService: ESIndexPatternService; -} +import { appContextService, ESIndexPatternSavedObjectService } from './services'; +import { ESIndexPatternService, AgentService } from './services'; +import { getAgentStatusById } from './services/agents'; export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; features?: FeaturesPluginSetup; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; } +export type IngestManagerStartDeps = object; + export interface IngestManagerAppContext { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; security?: SecurityPluginSetup; @@ -66,6 +62,8 @@ export interface IngestManagerAppContext { savedObjects: SavedObjectsServiceStart; } +export type IngestManagerSetupContract = void; + const allSavedObjectTypes = [ OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, @@ -76,7 +74,22 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; -export class IngestManagerPlugin implements Plugin { +/** + * Describes public IngestManager plugin contract returned at the `startup` stage. + */ +export interface IngestManagerStartContract { + esIndexPatternService: ESIndexPatternService; + agentService: AgentService; +} + +export class IngestManagerPlugin + implements + Plugin< + IngestManagerSetupContract, + IngestManagerStartContract, + IngestManagerSetupDeps, + IngestManagerStartDeps + > { private config$: Observable; private security: SecurityPluginSetup | undefined; @@ -84,14 +97,13 @@ export class IngestManagerPlugin implements Plugin { this.config$ = this.initializerContext.config.create(); } - public async setup( - core: CoreSetup, - deps: IngestManagerSetupDeps - ): Promise> { + public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { if (deps.security) { this.security = deps.security; } + registerEncryptedSavedObjects(deps.encryptedSavedObjects); + // Register feature // TODO: Flesh out privileges if (deps.features) { @@ -130,6 +142,7 @@ export class IngestManagerPlugin implements Plugin { // Register routes registerAgentConfigRoutes(router); registerDatasourceRoutes(router); + registerDataStreamRoutes(router); // Conditional routes if (config.epm.enabled) { @@ -146,9 +159,6 @@ export class IngestManagerPlugin implements Plugin { basePath: core.http.basePath, }); } - return deepFreeze({ - esIndexPatternService: new ESIndexPatternSavedObjectService(), - }); } public async start( @@ -163,6 +173,12 @@ export class IngestManagerPlugin implements Plugin { config$: this.config$, savedObjects: core.savedObjects, }); + return { + esIndexPatternService: new ESIndexPatternSavedObjectService(), + agentService: { + getAgentStatusById, + }, + }; } public async stop() { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts index 76247c338a24f..bcb9a7797f26a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts @@ -10,8 +10,7 @@ import { RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; -import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { ActionsService } from '../../services/agents'; import { AgentAction } from '../../../common/types/models'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 89c827abe30ec..5820303e2a1a7 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -14,6 +14,7 @@ import { PostAgentEnrollResponse, PostAgentUnenrollResponse, GetAgentStatusResponse, + PutAgentReassignResponse, } from '../../../common/types'; import { GetAgentsRequestSchema, @@ -25,6 +26,7 @@ import { PostAgentEnrollRequestSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, + PutAgentReassignRequestSchema, } from '../../types'; import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; @@ -293,60 +295,36 @@ export const getAgentsHandler: RequestHandler< } }; -export const postAgentsUnenrollHandler: RequestHandler< - undefined, +export const postAgentsUnenrollHandler: RequestHandler> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + await AgentService.unenrollAgent(soClient, request.params.agentId); + + const body: PostAgentUnenrollResponse = { + success: true, + }; + return response.ok({ body }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const putAgentsReassignHandler: RequestHandler< + TypeOf, undefined, - TypeOf + TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { - const kuery = (request.body as { kuery: string }).kuery; - let toUnenrollIds: string[] = (request.body as { ids: string[] }).ids || []; - - if (kuery) { - let hasMore = true; - let page = 1; - while (hasMore) { - const { agents } = await AgentService.listAgents(soClient, { - page: page++, - perPage: 100, - kuery, - showInactive: true, - }); - if (agents.length === 0) { - hasMore = false; - } - const agentIds = agents.filter(a => a.active).map(a => a.id); - toUnenrollIds = toUnenrollIds.concat(agentIds); - } - } - const results = (await AgentService.unenrollAgents(soClient, toUnenrollIds)).map( - ({ - success, - id, - error, - }): { - success: boolean; - id: string; - action: 'unenrolled'; - error?: { - message: string; - }; - } => { - return { - success, - id, - action: 'unenrolled', - error: error && { - message: error.message, - }, - }; - } - ); + await AgentService.reassignAgent(soClient, request.params.agentId, request.body.config_id); - const body: PostAgentUnenrollResponse = { - results, - success: results.every(result => result.success), + const body: PutAgentReassignResponse = { + success: true, }; return response.ok({ body }); } catch (e) { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index ac27e47db155e..78bb178dce402 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -23,6 +23,7 @@ import { PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PostNewAgentActionRequestSchema, + PutAgentReassignRequestSchema, } from '../../types'; import { getAgentsHandler, @@ -35,6 +36,7 @@ import { postAgentsUnenrollHandler, getAgentStatusForConfigHandler, getInternalUserSOClient, + putAgentsReassignHandler, } from './handlers'; import { postAgentAcksHandlerBuilder } from './acks_handlers'; import * as AgentService from '../../services/agents'; @@ -135,6 +137,15 @@ export const registerRoutes = (router: IRouter) => { postAgentsUnenrollHandler ); + router.put( + { + path: AGENT_API_ROUTES.REASSIGN_PATTERN, + validate: PutAgentReassignRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + putAgentsReassignHandler + ); + // Get agent events router.get( { diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts new file mode 100644 index 0000000000000..a24518d644c4c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -0,0 +1,125 @@ +/* + * 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 { RequestHandler } from 'src/core/server'; +import { DataStream } from '../../types'; +import { GetDataStreamsResponse } from '../../../common'; + +const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*'; + +export const getListHandler: RequestHandler = async (context, request, response) => { + const callCluster = context.core.elasticsearch.dataClient.callAsCurrentUser; + + try { + // Get stats (size on disk) of all potentially matching indices + const { indices: indexStats } = await callCluster('indices.stats', { + index: DATA_STREAM_INDEX_PATTERN, + metric: ['store'], + }); + + // Get all matching indices and info about each + // This returns the top 100,000 indices (as buckets) by last activity + const { + aggregations: { + index: { buckets: indexResults }, + }, + } = await callCluster('search', { + index: DATA_STREAM_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + must: [ + { + exists: { + field: 'stream.namespace', + }, + }, + { + exists: { + field: 'stream.dataset', + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 100000, + order: { + last_activity: 'desc', + }, + }, + aggs: { + dataset: { + terms: { + field: 'stream.dataset', + size: 1, + }, + }, + namespace: { + terms: { + field: 'stream.namespace', + size: 1, + }, + }, + type: { + terms: { + field: 'stream.type', + size: 1, + }, + }, + package: { + terms: { + field: 'event.module', + size: 1, + }, + }, + last_activity: { + max: { + field: '@timestamp', + }, + }, + }, + }, + }, + }, + }); + + const dataStreams: DataStream[] = (indexResults as any[]).map(result => { + const { + key: indexName, + dataset: { buckets: datasetBuckets }, + namespace: { buckets: namespaceBuckets }, + type: { buckets: typeBuckets }, + package: { buckets: packageBuckets }, + last_activity: { value_as_string: lastActivity }, + } = result; + return { + index: indexName, + dataset: datasetBuckets.length ? datasetBuckets[0].key : '', + namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', + type: typeBuckets.length ? typeBuckets[0].key : '', + package: packageBuckets.length ? packageBuckets[0].key : '', + last_activity: lastActivity, + size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, + }; + }); + + const body: GetDataStreamsResponse = { + data_streams: dataStreams, + }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts new file mode 100644 index 0000000000000..39502eba89a6a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { PLUGIN_ID, DATA_STREAM_API_ROUTES } from '../../constants'; +import { getListHandler } from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // List of data streams + router.get( + { + path: DATA_STREAM_API_ROUTES.LIST_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getListHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index 33d75f3ab82cd..8a186c5485024 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -5,6 +5,7 @@ */ export { registerRoutes as registerAgentConfigRoutes } from './agent_config'; export { registerRoutes as registerDatasourceRoutes } from './datasource'; +export { registerRoutes as registerDataStreamRoutes } from './data_streams'; export { registerRoutes as registerEPMRoutes } from './epm'; export { registerRoutes as registerSetupRoutes } from './setup'; export { registerRoutes as registerAgentRoutes } from './agent'; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 5c66f9008e2a3..837e73b966feb 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -5,7 +5,7 @@ */ import { RequestHandler } from 'src/core/server'; import { outputService } from '../../services'; -import { CreateFleetSetupResponse } from '../../types'; +import { CreateFleetSetupResponse } from '../../../common'; import { setupIngestManager, setupFleet } from '../../services/setup'; export const getFleetSetupHandler: RequestHandler = async (context, request, response) => { diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index a2c641503e825..edc9a0a268161 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -5,7 +5,6 @@ */ import { IRouter } from 'src/core/server'; import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; -import { GetFleetSetupRequestSchema, CreateFleetSetupRequestSchema } from '../../types'; import { getFleetSetupHandler, createFleetSetupHandler, @@ -28,7 +27,7 @@ export const registerRoutes = (router: IRouter) => { router.get( { path: FLEET_SETUP_API_ROUTES.INFO_PATTERN, - validate: GetFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getFleetSetupHandler @@ -38,7 +37,7 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: FLEET_SETUP_API_ROUTES.CREATE_PATTERN, - validate: CreateFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, createFleetSetupHandler diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index dc0b4695603e4..37a00228443e1 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -13,6 +13,7 @@ import { AGENT_ACTION_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, } from './constants'; +import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; /* * Saved object mappings @@ -35,7 +36,7 @@ export const savedObjectMappings = { last_checkin: { type: 'date' }, config_revision: { type: 'integer' }, config_newest_revision: { type: 'integer' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 + default_api_key_id: { type: 'keyword' }, default_api_key: { type: 'keyword' }, updated_at: { type: 'date' }, current_error_events: { type: 'text' }, @@ -45,8 +46,7 @@ export const savedObjectMappings = { properties: { agent_id: { type: 'keyword' }, type: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - data: { type: 'flattened' }, + data: { type: 'binary' }, sent_at: { type: 'date' }, created_at: { type: 'date' }, }, @@ -83,7 +83,6 @@ export const savedObjectMappings = { properties: { name: { type: 'keyword' }, type: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 api_key: { type: 'binary' }, api_key_id: { type: 'keyword' }, config_id: { type: 'keyword' }, @@ -100,8 +99,6 @@ export const savedObjectMappings = { is_default: { type: 'boolean' }, hosts: { type: 'keyword' }, ca_sha256: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - api_key: { type: 'keyword' }, fleet_enroll_username: { type: 'binary' }, fleet_enroll_password: { type: 'binary' }, config: { type: 'flattened' }, @@ -150,6 +147,7 @@ export const savedObjectMappings = { name: { type: 'keyword' }, version: { type: 'keyword' }, internal: { type: 'boolean' }, + removable: { type: 'boolean' }, es_index_patterns: { dynamic: false, type: 'object', @@ -164,3 +162,61 @@ export const savedObjectMappings = { }, }, }; + +export function registerEncryptedSavedObjects( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + // Encrypted saved objects + encryptedSavedObjects.registerType({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['api_key']), + attributesToExcludeFromAAD: new Set([ + 'name', + 'type', + 'api_key_id', + 'config_id', + 'created_at', + 'updated_at', + 'expire_at', + 'active', + ]), + }); + encryptedSavedObjects.registerType({ + type: OUTPUT_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['fleet_enroll_username', 'fleet_enroll_password']), + attributesToExcludeFromAAD: new Set([ + 'name', + 'type', + 'is_default', + 'hosts', + 'ca_sha256', + 'config', + ]), + }); + encryptedSavedObjects.registerType({ + type: AGENT_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['default_api_key']), + attributesToExcludeFromAAD: new Set([ + 'shared_id', + 'type', + 'active', + 'enrolled_at', + 'access_api_key_id', + 'version', + 'user_provided_metadata', + 'local_metadata', + 'config_id', + 'last_updated', + 'last_checkin', + 'config_revision', + 'config_newest_revision', + 'updated_at', + 'current_error_events', + ]), + }); + encryptedSavedObjects.registerType({ + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['data']), + attributesToExcludeFromAAD: new Set(['agent_id', 'type', 'sent_at', 'created_at']), + }); +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts index b4c1f09015a69..a8fada00e25da 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -5,7 +5,9 @@ */ import Boom from 'boom'; import { SavedObjectsBulkResponse } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks'; + import { Agent, AgentAction, @@ -14,10 +16,31 @@ import { } from '../../../common/types/models'; import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; import { acknowledgeAgentActions } from './acks'; +import { appContextService } from '../app_context'; +import { IngestManagerAppContext } from '../../plugin'; describe('test agent acks services', () => { it('should succeed on valid and matched actions', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockStartEncryptedSOClient = encryptedSavedObjectsMock.createStart(); + appContextService.start(({ + encryptedSavedObjects: mockStartEncryptedSOClient, + } as unknown) as IngestManagerAppContext); + + mockStartEncryptedSOClient.getDecryptedAsInternalUser.mockReturnValue( + Promise.resolve({ + id: 'action1', + references: [], + type: 'agent_actions', + attributes: { + type: 'CONFIG_CHANGE', + agent_id: 'id', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + }) + ); mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts index f2e671c6dbaa8..c739007952389 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts @@ -6,17 +6,17 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; -import { AgentAction, AgentActionSOAttributes } from '../../../common/types/models'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { AgentAction } from '../../../common/types/models'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - const newAgentAction: AgentActionSOAttributes = { + const newAgentAction: Omit = { agent_id: 'agentid', type: 'CONFIG_CHANGE', - data: 'data', + data: { content: 'data' }, sent_at: '2020-03-14T19:45:02.620Z', created_at: '2020-03-14T19:45:02.620Z', }; @@ -31,7 +31,7 @@ describe('test agent actions services', () => { .calls[0][1] as unknown) as AgentAction; expect(createdAction).toBeDefined(); expect(createdAction?.type).toEqual(newAgentAction.type); - expect(createdAction?.data).toEqual(newAgentAction.data); + expect(createdAction?.data).toEqual(JSON.stringify(newAgentAction.data)); expect(createdAction?.sent_at).toEqual(newAgentAction.sent_at); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index a8ef0820f8d9f..1bb177e54282d 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -8,16 +8,21 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { Agent, AgentAction, AgentActionSOAttributes } from '../../../common/types/models'; import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { savedObjectToAgentAction } from './saved_objects'; +import { appContextService } from '../app_context'; export async function createAgentAction( soClient: SavedObjectsClientContract, - newAgentAction: AgentActionSOAttributes + newAgentAction: Omit ): Promise { const so = await soClient.create(AGENT_ACTION_SAVED_OBJECT_TYPE, { ...newAgentAction, + data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, }); - return savedObjectToAgentAction(so); + const agentAction = savedObjectToAgentAction(so); + agentAction.data = newAgentAction.data; + + return agentAction; } export async function getAgentActionsForCheckin( @@ -29,21 +34,47 @@ export async function getAgentActionsForCheckin( filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * and ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id:${agentId}`, }); - return res.saved_objects.map(savedObjectToAgentAction); + return Promise.all( + res.saved_objects.map(async so => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + AGENT_ACTION_SAVED_OBJECT_TYPE, + so.id + ) + ); + }) + ); } export async function getAgentActionByIds( soClient: SavedObjectsClientContract, actionIds: string[] ) { - const res = await soClient.bulkGet( - actionIds.map(actionId => ({ - id: actionId, - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - })) - ); + const actions = ( + await soClient.bulkGet( + actionIds.map(actionId => ({ + id: actionId, + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + })) + ) + ).saved_objects.map(savedObjectToAgentAction); - return res.saved_objects.map(savedObjectToAgentAction); + return Promise.all( + actions.map(async action => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + AGENT_ACTION_SAVED_OBJECT_TYPE, + action.id + ) + ); + }) + ); } export interface ActionsService { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts index d98052ea87e86..72a86d7c8158e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts @@ -53,12 +53,12 @@ describe('Agent checkin service', () => { agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config1', revision: 2, }, - }), + }, }, ] ); @@ -80,24 +80,24 @@ describe('Agent checkin service', () => { agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config2', revision: 2, }, - }), + }, }, { id: 'action1', agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config1', revision: 1, }, - }), + }, }, ] ); @@ -118,5 +118,19 @@ describe('Agent checkin service', () => { expect(res).toBeTruthy(); }); + + it('should return true if this agent has no revision currently set', () => { + const res = shouldCreateConfigAction( + getAgent({ + config_id: 'config1', + last_checkin: '2018-01-02T00:00:00', + config_revision: null, + config_newest_revision: 2, + }), + [] + ); + + expect(res).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index 9a2b3f22b9431..c96a81ed9b758 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -17,6 +17,7 @@ import { agentConfigService } from '../agent_config'; import * as APIKeysService from '../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgentActionsForCheckin, createAgentAction } from './actions'; +import { appContextService } from '../app_context'; export async function agentCheckin( soClient: SavedObjectsClientContract, @@ -27,7 +28,6 @@ export async function agentCheckin( const updateData: { last_checkin: string; default_api_key?: string; - actions?: AgentAction[]; local_metadata?: string; current_error_events?: string; } = { @@ -38,11 +38,17 @@ export async function agentCheckin( // Generate new agent config if config is updated if (agent.config_id && shouldCreateConfigAction(agent, actions)) { + const { + attributes: { default_api_key: defaultApiKey }, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); + const config = await agentConfigService.getFullConfig(soClient, agent.config_id); if (config) { // Assign output API keys // We currently only support default ouput - if (!agent.default_api_key) { + if (!defaultApiKey) { updateData.default_api_key = await APIKeysService.generateOutputApiKey( soClient, 'default', @@ -50,7 +56,7 @@ export async function agentCheckin( ); } // Mutate the config to set the api token for this agent - config.outputs.default.api_key = agent.default_api_key || updateData.default_api_key; + config.outputs.default.api_key = defaultApiKey || updateData.default_api_key; const configChangeAction = await createAgentAction(soClient, { agent_id: agent.id, @@ -62,9 +68,6 @@ export async function agentCheckin( actions.push(configChangeAction); } } - if (localMetadata) { - updateData.local_metadata = JSON.stringify(localMetadata); - } const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, events); @@ -156,9 +159,13 @@ export function shouldCreateConfigAction(agent: Agent, actions: AgentAction[]): } const isAgentConfigOutdated = - agent.config_revision && - agent.config_newest_revision && - agent.config_revision < agent.config_newest_revision; + // Config reassignment + (!agent.config_revision && agent.config_newest_revision) || + // new revision of a config + (agent.config_revision && + agent.config_newest_revision && + agent.config_revision < agent.config_newest_revision); + if (!isAgentConfigOutdated) { return false; } @@ -168,7 +175,7 @@ export function shouldCreateConfigAction(agent: Agent, actions: AgentAction[]): return false; } - const data = JSON.parse(action.data); + const { data } = action; return ( data.config.id === agent.config_id && data.config.revision === agent.config_newest_revision diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts index c95c9ecc2a1d8..257091af0ebd0 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts @@ -13,3 +13,4 @@ export * from './status'; export * from './crud'; export * from './update'; export * from './actions'; +export * from './reassign'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts new file mode 100644 index 0000000000000..f8142af376eb3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts @@ -0,0 +1,28 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; +import Boom from 'boom'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentSOAttributes } from '../../types'; +import { agentConfigService } from '../agent_config'; + +export async function reassignAgent( + soClient: SavedObjectsClientContract, + agentId: string, + newConfigId: string +) { + const config = await agentConfigService.get(soClient, newConfigId); + if (!config) { + throw Boom.notFound(`Agent Configuration not found: ${newConfigId}`); + } + + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + config_id: newConfigId, + config_revision: null, + config_newest_revision: config.revision, + }); +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts index aa88520740687..b182662e0fb4e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts @@ -38,5 +38,6 @@ export function savedObjectToAgentAction(so: SavedObject { + it('should return inactive when agent is not active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: false, + local_metadata: '{}', + user_provided_metadata: '{}', + }, + } as SavedObject); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('inactive'); + }); + + it('should return online when agent is active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: true, + local_metadata: '{}', + user_provided_metadata: '{}', + }, + } as SavedObject); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('online'); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 21e200d701e69..fce989cea3248 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import { listAgents } from './crud'; +import { getAgent, listAgents } from './crud'; import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentStatus, Agent } from '../../types'; @@ -17,6 +17,14 @@ import { } from '../../constants'; import { AgentStatusKueryHelper } from '../../../common/services'; +export async function getAgentStatusById( + soClient: SavedObjectsClientContract, + agentId: string +): Promise { + const agent = await getAgent(soClient, agentId); + return getAgentStatus(agent); +} + export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus { const { type, last_checkin: lastCheckIn } = agent; const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); @@ -59,7 +67,7 @@ export async function getAgentStatusForConfig( AgentStatusKueryHelper.buildKueryForOfflineAgents(), ].map(kuery => listAgents(soClient, { - showInactive: true, + showInactive: false, perPage: 0, page: 1, kuery: configId diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index 18af9fd4de73f..ee7e08d741035 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -10,42 +10,15 @@ import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; import * as APIKeyService from '../api_keys'; -export async function unenrollAgents( - soClient: SavedObjectsClientContract, - toUnenrollIds: string[] -) { - const response = []; - for (const id of toUnenrollIds) { - try { - await unenrollAgent(soClient, id); - response.push({ - id, - success: true, - }); - } catch (error) { - response.push({ - id, - error, - success: false, - }); - } - } - - return response; -} - -async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { +export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = await getAgent(soClient, agentId); await Promise.all([ agent.access_api_key_id ? APIKeyService.invalidateAPIKey(soClient, agent.access_api_key_id) : undefined, - agent.default_api_key - ? APIKeyService.invalidateAPIKey( - soClient, - APIKeyService.parseApiKey(agent.default_api_key).apiKeyId - ) + agent.default_api_key_id + ? APIKeyService.invalidateAPIKey(soClient, agent.default_api_key_id) : undefined, ]); await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index 59d0ad31d1a64..948e518dff5b4 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { unenrollAgents } from './unenroll'; +import { unenrollAgent } from './unenroll'; import { agentConfigService } from '../agent_config'; export async function updateAgentsForConfigId( @@ -55,9 +55,8 @@ export async function unenrollForConfigId(soClient: SavedObjectsClientContract, if (agents.length === 0) { hasMore = false; } - await unenrollAgents( - soClient, - agents.map(a => a.id) - ); + for (const agent of agents) { + await unenrollAgent(soClient, agent.id); + } } } diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index a6a2db8be4e9d..c9ead09b0908d 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -10,6 +10,7 @@ import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKey } from './security'; import { agentConfigService } from '../agent_config'; +import { appContextService } from '../app_context'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -45,9 +46,13 @@ export async function listEnrollmentApiKeys( } export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { - return savedObjectToEnrollmentApiKey( - await soClient.get(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id) - ); + const so = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + id + ); + return savedObjectToEnrollmentApiKey(so); } /** @@ -120,16 +125,19 @@ export async function generateEnrollmentAPIKey( const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); - return savedObjectToEnrollmentApiKey( - await soClient.create(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, { + const so = await soClient.create( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + { active: true, api_key_id: key.id, api_key: apiKey, name, config_id: configId, created_at: new Date().toISOString(), - }) + } ); + + return getEnrollmentAPIKey(soClient, so.id); } function savedObjectToEnrollmentApiKey({ diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index a0a7c8dd7c05a..e917d2edd1309 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -34,6 +34,9 @@ class AppContextService { public stop() {} public getEncryptedSavedObjects() { + if (!this.encryptedSavedObjects) { + throw new Error('Encrypted saved object start service not set.'); + } return this.encryptedSavedObjects; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 166983fbccc35..5cf1f241a709f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -28,9 +28,6 @@ exports[`tests loading base.yml: base.yml 1`] = ` } }, "mappings": { - "_meta": { - "package": "foo" - }, "dynamic_templates": [ { "strings_as_keyword": { @@ -123,9 +120,6 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` } }, "mappings": { - "_meta": { - "package": "foo" - }, "dynamic_templates": [ { "strings_as_keyword": { @@ -218,9 +212,6 @@ exports[`tests loading system.yml: system.yml 1`] = ` } }, "mappings": { - "_meta": { - "package": "foo" - }, "dynamic_templates": [ { "strings_as_keyword": { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 560ddfc1f6885..4df626259ece7 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AssetReference, - Dataset, - RegistryPackage, - IngestAssetType, - ElasticsearchAssetType, -} from '../../../../types'; +import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types'; import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; @@ -22,7 +16,7 @@ export const installTemplates = async ( callCluster: CallESAsCurrentUser, pkgName: string, pkgVersion: string -) => { +): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global template installPreBuiltTemplates(pkgName, pkgVersion, callCluster); @@ -30,7 +24,7 @@ export const installTemplates = async ( // build templates per dataset from yml files const datasets = registryPackage.datasets; if (datasets) { - const templates = datasets.reduce>>((acc, dataset) => { + const installTemplatePromises = datasets.reduce>>((acc, dataset) => { acc.push( installTemplateForDataset({ pkg: registryPackage, @@ -40,7 +34,9 @@ export const installTemplates = async ( ); return acc; }, []); - return Promise.all(templates).then(results => results.flat()); + + const res = await Promise.all(installTemplatePromises); + return res.flat(); } return []; }; @@ -84,7 +80,7 @@ export async function installTemplateForDataset({ pkg: RegistryPackage; callCluster: CallESAsCurrentUser; dataset: Dataset; -}): Promise { +}): Promise { const fields = await loadFieldsFromYaml(pkg, dataset.path); return installTemplate({ callCluster, @@ -104,7 +100,7 @@ export async function installTemplate({ fields: Field[]; dataset: Dataset; packageVersion: string; -}): Promise { +}): Promise { const mappings = generateMappings(processFields(fields)); const templateName = generateTemplateName(dataset); let pipelineName; @@ -122,6 +118,8 @@ export async function installTemplate({ body: template, }); - // The id of a template is its name - return { id: templateName, type: IngestAssetType.IndexTemplate }; + return { + templateName, + indexTemplate: template, + }; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 22a61d2bdfb7c..46b6923962462 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -5,24 +5,30 @@ */ import { Field, Fields } from '../../fields/field'; -import { Dataset, IndexTemplate } from '../../../../types'; +import { + Dataset, + CallESAsCurrentUser, + TemplateRef, + IndexTemplate, + IndexTemplateMappings, +} from '../../../../types'; import { getDatasetAssetBaseName } from '../index'; interface Properties { [key: string]: any; } -interface Mappings { - properties: any; -} - -interface Mapping { - [key: string]: any; -} interface MultiFields { [key: string]: object; } +export interface IndexTemplateMapping { + [key: string]: any; +} +export interface CurrentIndex { + indexName: string; + indexTemplate: IndexTemplate; +} const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; @@ -34,7 +40,7 @@ const DEFAULT_IGNORE_ABOVE = 1024; export function getTemplate( type: string, templateName: string, - mappings: Mappings, + mappings: IndexTemplateMappings, pipelineName?: string | undefined ): IndexTemplate { const template = getBaseTemplate(type, templateName, mappings); @@ -52,7 +58,7 @@ export function getTemplate( * * @param fields */ -export function generateMappings(fields: Field[]): Mappings { +export function generateMappings(fields: Field[]): IndexTemplateMappings { const props: Properties = {}; // TODO: this can happen when the fields property in fields.yml is present but empty // Maybe validation should be moved to fields/field.ts @@ -140,8 +146,8 @@ function generateMultiFields(fields: Fields): MultiFields { return multiFields; } -function generateKeywordMapping(field: Field): Mapping { - const mapping: Mapping = { +function generateKeywordMapping(field: Field): IndexTemplateMapping { + const mapping: IndexTemplateMapping = { ignore_above: DEFAULT_IGNORE_ABOVE, }; if (field.ignore_above) { @@ -150,8 +156,8 @@ function generateKeywordMapping(field: Field): Mapping { return mapping; } -function generateTextMapping(field: Field): Mapping { - const mapping: Mapping = {}; +function generateTextMapping(field: Field): IndexTemplateMapping { + const mapping: IndexTemplateMapping = {}; if (field.analyzer) { mapping.analyzer = field.analyzer; } @@ -200,7 +206,11 @@ export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record return patterns; } -function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate { +function getBaseTemplate( + type: string, + templateName: string, + mappings: IndexTemplateMappings +): IndexTemplate { return { // We need to decide which order we use for the templates order: 1, @@ -234,10 +244,6 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings) }, }, mappings: { - // To be filled with interesting information about this specific index - _meta: { - package: 'foo', - }, // All the dynamic field mappings dynamic_templates: [ // This makes sure all mappings are keywords by default @@ -261,3 +267,112 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings) aliases: {}, }; } + +export const updateCurrentWriteIndices = async ( + callCluster: CallESAsCurrentUser, + templates: TemplateRef[] +): Promise => { + if (!templates) return; + + const allIndices = await queryIndicesFromTemplates(callCluster, templates); + return updateAllIndices(allIndices, callCluster); +}; + +const queryIndicesFromTemplates = async ( + callCluster: CallESAsCurrentUser, + templates: TemplateRef[] +): Promise => { + const indexPromises = templates.map(template => { + return getIndices(callCluster, template); + }); + const indexObjects = await Promise.all(indexPromises); + return indexObjects.filter(item => item !== undefined).flat(); +}; + +const getIndices = async ( + callCluster: CallESAsCurrentUser, + template: TemplateRef +): Promise => { + const { templateName, indexTemplate } = template; + const res = await callCluster('search', getIndexQuery(templateName)); + const indices: any[] = res?.aggregations?.index.buckets; + if (indices) { + return indices.map(index => ({ + indexName: index.key, + indexTemplate, + })); + } +}; + +const updateAllIndices = async ( + indexNameWithTemplates: CurrentIndex[], + callCluster: CallESAsCurrentUser +): Promise => { + const updateIndexPromises = indexNameWithTemplates.map(({ indexName, indexTemplate }) => { + return updateExistingIndex({ indexName, callCluster, indexTemplate }); + }); + await Promise.all(updateIndexPromises); +}; +const updateExistingIndex = async ({ + indexName, + callCluster, + indexTemplate, +}: { + indexName: string; + callCluster: CallESAsCurrentUser; + indexTemplate: IndexTemplate; +}) => { + const { settings, mappings } = indexTemplate; + // try to update the mappings first + // for now we assume updates are compatible + try { + await callCluster('indices.putMapping', { + index: indexName, + body: mappings, + }); + } catch (err) { + throw new Error('incompatible mappings update'); + } + // update settings after mappings was successful to ensure + // pointing to theme new pipeline is safe + // for now, only update the pipeline + if (!settings.index.default_pipeline) return; + try { + await callCluster('indices.putSettings', { + index: indexName, + body: { index: { default_pipeline: settings.index.default_pipeline } }, + }); + } catch (err) { + throw new Error('incompatible settings update'); + } +}; + +const getIndexQuery = (templateName: string) => ({ + index: `${templateName}-*`, + size: 0, + body: { + query: { + bool: { + must: [ + { + exists: { + field: 'stream.namespace', + }, + }, + { + exists: { + field: 'stream.dataset', + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 0a7642752b3e9..06f3decdbbe6f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -12,15 +12,19 @@ import { KibanaAssetType, CallESAsCurrentUser, DefaultPackages, + ElasticsearchAssetType, + IngestAssetType, } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getObject } from './get_objects'; -import { getInstallation } from './index'; +import { getInstallation, getInstallationObject } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; +import { deleteAssetsByType, deleteKibanaSavedObjectsAssets } from './remove'; +import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -89,44 +93,84 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); + // see if some version of this package is already installed + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const reinstall = pkgVersion === installedPkg?.attributes.version; + const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); - const { internal = false } = registryPackageInfo; + const { internal = false, removable = true } = registryPackageInfo; - const installKibanaAssetsPromise = installKibanaAssets({ - savedObjectsClient, - pkgName, - pkgVersion, - }); - const installPipelinePromises = installPipelines(registryPackageInfo, callCluster); - const installTemplatePromises = installTemplates( + // delete the previous version's installation's SO kibana assets before installing new ones + // in case some assets were removed in the new version + if (installedPkg) { + try { + await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); + } catch (err) { + // some assets may not exist if deleting during a failed update + } + } + + const [installedKibanaAssets, installedPipelines] = await Promise.all([ + installKibanaAssets({ + savedObjectsClient, + pkgName, + pkgVersion, + }), + installPipelines(registryPackageInfo, callCluster), + // index patterns and ilm policies are not currently associated with a particular package + // so we do not save them in the package saved object state. + installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), + // currenly only the base package has an ILM policy + // at some point ILM policies can be installed/modified + // per dataset and we should then save them + installILMPolicy(pkgName, pkgVersion, callCluster), + ]); + + // install or update the templates + const installedTemplates = await installTemplates( registryPackageInfo, callCluster, pkgName, pkgVersion ); + const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // index patterns and ilm policies are not currently associated with a particular package - // so we do not save them in the package saved object state. at some point ILM policies can be installed/modified - // per dataset and we should then save them - await installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); - // currenly only the base package has an ILM policy - await installILMPolicy(pkgName, pkgVersion, callCluster); - - const res = await Promise.all([ - installKibanaAssetsPromise, - installPipelinePromises, - installTemplatePromises, - ]); + // get template refs to save + const installedTemplateRefs = installedTemplates.map(template => ({ + id: template.templateName, + type: IngestAssetType.IndexTemplate, + })); - const toSaveAssetRefs: AssetReference[] = res.flat(); - const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // Save those references in the package manager's state saved object - return await saveInstallationReferences({ + if (installedPkg) { + // update current index for every index template created + await updateCurrentWriteIndices(callCluster, installedTemplates); + if (!reinstall) { + try { + // delete the previous version's installation's pipelines + // this must happen after the template is updated + await deleteAssetsByType({ + savedObjectsClient, + callCluster, + installedObjects: installedPkg.attributes.installed, + assetType: ElasticsearchAssetType.ingestPipeline, + }); + } catch (err) { + throw new Error(err.message); + } + } + } + const toSaveAssetRefs: AssetReference[] = [ + ...installedKibanaAssets, + ...installedPipelines, + ...installedTemplateRefs, + ]; + // Save references to installed assets in the package's saved object state + return saveInstallationReferences({ savedObjectsClient, - pkgkey, pkgName, pkgVersion, internal, + removable, toSaveAssetRefs, toSaveESIndexPatterns, }); @@ -154,10 +198,10 @@ export async function installKibanaAssets(options: { export async function saveInstallationReferences(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; pkgName: string; pkgVersion: string; internal: boolean; + removable: boolean; toSaveAssetRefs: AssetReference[]; toSaveESIndexPatterns: Record; }) { @@ -166,36 +210,25 @@ export async function saveInstallationReferences(options: { pkgName, pkgVersion, internal, + removable, toSaveAssetRefs, toSaveESIndexPatterns, } = options; - const installation = await getInstallation({ savedObjectsClient, pkgName }); - const savedAssetRefs = installation?.installed || []; - const toInstallESIndexPatterns = Object.assign( - installation?.es_index_patterns || {}, - toSaveESIndexPatterns - ); - - const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => { - const hasRef = current.find(c => c.id === pending.id && c.type === pending.type); - if (!hasRef) current.push(pending); - return current; - }; - const toInstallAssetsRefs = toSaveAssetRefs.reduce(mergeRefsReducer, savedAssetRefs); await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, { - installed: toInstallAssetsRefs, - es_index_patterns: toInstallESIndexPatterns, + installed: toSaveAssetRefs, + es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, internal, + removable, }, { id: pkgName, overwrite: true } ); - return toInstallAssetsRefs; + return toSaveAssetRefs; } async function installKibanaSavedObjects({ diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index a30acb97b99cf..498796438c6c8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -20,7 +20,10 @@ export async function removeInstallation(options: { // TODO: the epm api should change to /name/version so we don't need to do this const [pkgName] = pkgkey.split('-'); const installation = await getInstallation({ savedObjectsClient, pkgName }); - const installedObjects = installation?.installed || []; + if (!installation) throw new Error('integration does not exist'); + if (installation.removable === false) + throw new Error(`The ${pkgName} integration is installed by default and cannot be removed`); + const installedObjects = installation.installed || []; // Delete the manager saved object with references to the asset objects // could also update with [] or some other state @@ -29,7 +32,17 @@ export async function removeInstallation(options: { // recreate or delete index patterns when a package is uninstalled await installIndexPatterns(savedObjectsClient); - // Delete the installed assets + // Delete the installed asset + await deleteAssets(installedObjects, savedObjectsClient, callCluster); + + // successful delete's in SO client return {}. return something more useful + return installedObjects; +} +async function deleteAssets( + installedObjects: AssetReference[], + savedObjectsClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser +) { const deletePromises = installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { @@ -40,22 +53,62 @@ export async function removeInstallation(options: { deleteTemplate(callCluster, id); } }); - await Promise.all([...deletePromises]); - - // successful delete's in SO client return {}. return something more useful - return installedObjects; + try { + await Promise.all([...deletePromises]); + } catch (err) { + throw new Error(err.message); + } } - async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise { // '*' shouldn't ever appear here, but it still would delete all ingest pipelines if (id && id !== '*') { - await callCluster('ingest.deletePipeline', { id }); + try { + await callCluster('ingest.deletePipeline', { id }); + } catch (err) { + throw new Error(`error deleting pipeline ${id}`); + } } } async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): Promise { // '*' shouldn't ever appear here, but it still would delete all templates if (name && name !== '*') { - await callCluster('indices.deleteTemplate', { name }); + try { + await callCluster('indices.deleteTemplate', { name }); + } catch { + throw new Error(`error deleting template ${name}`); + } } } + +export async function deleteAssetsByType({ + savedObjectsClient, + callCluster, + installedObjects, + assetType, +}: { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + installedObjects: AssetReference[]; + assetType: ElasticsearchAssetType; +}) { + const toDelete = installedObjects.filter(asset => asset.type === assetType); + try { + await deleteAssets(toDelete, savedObjectsClient, callCluster); + } catch (err) { + throw new Error(err.message); + } +} + +export async function deleteKibanaSavedObjectsAssets( + savedObjectsClient: SavedObjectsClientContract, + installedObjects: AssetReference[] +) { + const deletePromises = installedObjects.map(({ id, type }) => { + const assetType = type as AssetType; + if (savedObjectTypes.includes(assetType)) { + savedObjectsClient.delete(assetType, id); + } + }); + await Promise.all(deletePromises); +} diff --git a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts index 167e22873979c..fc2fe6d1c40e8 100644 --- a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts +++ b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts @@ -4,15 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract } from 'kibana/server'; -import { getInstallation } from './epm/packages/get'; - -export interface ESIndexPatternService { - getESIndexPattern( - savedObjectsClient: SavedObjectsClientContract, - pkgName: string, - datasetPath: string - ): Promise; -} +import { getInstallation } from './epm/packages'; +import { ESIndexPatternService } from '../../server'; export class ESIndexPatternSavedObjectService implements ESIndexPatternService { public async getESIndexPattern( diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index d64f1b0c2b6fb..1b0f174cc1a8e 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -3,8 +3,34 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentStatus } from '../../common/types/models'; + export { appContextService } from './app_context'; -export { ESIndexPatternService, ESIndexPatternSavedObjectService } from './es_index_pattern'; +export { ESIndexPatternSavedObjectService } from './es_index_pattern'; + +/** + * Service to return the index pattern of EPM packages + */ +export interface ESIndexPatternService { + getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise; +} + +/** + * A service that provides exported functions that return information about an Agent + */ +export interface AgentService { + /** + * Return the status by the Agent's id + * @param soClient + * @param agentId + */ + getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise; +} // Saved object services export { datasourceService } from './datasource'; diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 1cd5622c0c7b0..aa5496cc836b7 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -22,6 +22,7 @@ export { AgentConfig, NewAgentConfig, AgentConfigStatus, + DataStream, Output, NewOutput, OutputType, @@ -47,6 +48,8 @@ export { RegistrySearchResults, RegistrySearchResult, DefaultPackages, + TemplateRef, + IndexTemplateMappings, } from '../../common'; export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index f94c02ccee40b..ac1679101312e 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -62,14 +62,18 @@ export const PostNewAgentActionRequestSchema = { }; export const PostAgentUnenrollRequestSchema = { - body: schema.oneOf([ - schema.object({ - kuery: schema.string(), - }), - schema.object({ - ids: schema.arrayOf(schema.string()), - }), - ]), + params: schema.object({ + agentId: schema.string(), + }), +}; + +export const PutAgentReassignRequestSchema = { + params: schema.object({ + agentId: schema.string(), + }), + body: schema.object({ + config_id: schema.string(), + }), }; export const GetOneAgentEventsRequestSchema = { diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts deleted file mode 100644 index 2244bcd44043f..0000000000000 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts +++ /dev/null @@ -1,13 +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. - */ - -export const GetFleetSetupRequestSchema = {}; - -export const CreateFleetSetupRequestSchema = {}; - -export interface CreateFleetSetupResponse { - isInitialized: boolean; -} diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts index c143cd3b35f91..42b607fa1c715 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts @@ -9,5 +9,4 @@ export * from './agent'; export * from './datasource'; export * from './epm'; export * from './enrollment_api_key'; -export * from './fleet_setup'; export * from './install_script'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss deleted file mode 100644 index 62a7f6b023f31..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss +++ /dev/null @@ -1,50 +0,0 @@ -.lnsConfigPanel__panel { - margin-bottom: $euiSizeS; -} - -.lnsConfigPanel__row { - background: $euiColorLightestShade; - padding: $euiSizeS; - border-radius: $euiBorderRadius; - - // Add margin to the top of the next same panel - & + & { - margin-top: $euiSizeS; - } -} - -.lnsConfigPanel__addLayerBtn { - color: transparentize($euiColorMediumShade, .3); - // Remove EuiButton's default shadow to make button more subtle - // sass-lint:disable-block no-important - box-shadow: none !important; - border: 1px dashed currentColor; -} - -.lnsConfigPanel__dimension { - @include euiFontSizeS; - background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - margin-top: $euiSizeXS; - overflow: hidden; -} - -.lnsConfigPanel__trigger { - max-width: 100%; - display: block; -} - -.lnsConfigPanel__triggerLink { - padding: $euiSizeS; - width: 100%; - display: flex; - align-items: center; - min-height: $euiSizeXXL; -} - -.lnsConfigPanel__popover { - line-height: 0; - flex-grow: 1; -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss similarity index 100% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss new file mode 100644 index 0000000000000..1965b51f97034 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss @@ -0,0 +1,7 @@ +.lnsConfigPanel__addLayerBtn { + color: transparentize($euiColorMediumShade, .3); + // Remove EuiButton's default shadow to make button more subtle + // sass-lint:disable-block no-important + box-shadow: none !important; + border: 1px dashed currentColor; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss new file mode 100644 index 0000000000000..254807d06d386 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss @@ -0,0 +1,9 @@ +.lnsDimensionPopover { + line-height: 0; + flex-grow: 1; +} + +.lnsDimensionPopover__trigger { + max-width: 100%; + display: block; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss new file mode 100644 index 0000000000000..8f09a358dd5e4 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss @@ -0,0 +1,4 @@ +@import 'chart_switch'; +@import 'config_panel'; +@import 'dimension_popover'; +@import 'layer_panel'; 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 new file mode 100644 index 0000000000000..3fbc42f9a25a0 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -0,0 +1,33 @@ +.lnsLayerPanel { + margin-bottom: $euiSizeS; +} + +.lnsLayerPanel__row { + background: $euiColorLightestShade; + padding: $euiSizeS; + border-radius: $euiBorderRadius; + + // Add margin to the top of the next same panel + & + & { + margin-top: $euiSizeS; + } +} + +.lnsLayerPanel__dimension { + @include euiFontSizeS; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + overflow: hidden; +} + +.lnsLayerPanel__triggerLink { + padding: $euiSizeS; + width: 100%; + display: flex; + align-items: center; + min-height: $euiSizeXXL; +} + diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx similarity index 98% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx index 6698c9e68b98c..3c61d270b1bcf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx @@ -5,13 +5,17 @@ */ import React from 'react'; -import { createMockVisualization, createMockFramePublicAPI, createMockDatasource } from '../mocks'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; +import { + createMockVisualization, + createMockFramePublicAPI, + createMockDatasource, +} from '../../mocks'; +import { EuiKeyPadMenuItem } from '@elastic/eui'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types'; +import { Action } from '../state_management'; import { ChartSwitch } from './chart_switch'; -import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; -import { EuiKeyPadMenuItemButton } from '@elastic/eui'; -import { Action } from './state_management'; describe('chart_switch', () => { function generateVisualization(id: string): jest.Mocked { @@ -129,7 +133,7 @@ describe('chart_switch', () => { function getMenuItem(subType: string, component: ReactWrapper) { showFlyout(component); return component - .find(EuiKeyPadMenuItemButton) + .find(EuiKeyPadMenuItem) .find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`) .first(); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx similarity index 97% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx index 5e2fced577724..1461449f3c1c8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx @@ -10,15 +10,15 @@ import { EuiPopover, EuiPopoverTitle, EuiKeyPadMenu, - EuiKeyPadMenuItemButton, + EuiKeyPadMenuItem, EuiButtonEmpty, } from '@elastic/eui'; import { flatten } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Visualization, FramePublicAPI, Datasource } from '../../types'; -import { Action } from './state_management'; -import { getSuggestions, switchToSuggestion, Suggestion } from './suggestion_helpers'; -import { trackUiEvent } from '../../lens_ui_telemetry'; +import { Visualization, FramePublicAPI, Datasource } from '../../../types'; +import { Action } from '../state_management'; +import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; interface VisualizationSelection { visualizationId: string; @@ -215,7 +215,7 @@ export function ChartSwitch(props: Props) { {(visualizationTypes || []).map(v => ( - {v.label}} role="menuitem" @@ -238,7 +238,7 @@ export function ChartSwitch(props: Props) { betaBadgeIconType={v.selection.dataLoss !== 'nothing' ? 'alert' : undefined} > - + ))} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx new file mode 100644 index 0000000000000..e5d3e93258c0a --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -0,0 +1,177 @@ +/* + * 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, { useMemo, memo } from 'react'; +import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Visualization } from '../../../types'; +import { ChartSwitch } from './chart_switch'; +import { LayerPanel } from './layer_panel'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { generateId } from '../../../id_generator'; +import { removeLayer, appendLayer } from './layer_actions'; +import { ConfigPanelWrapperProps } from './types'; + +export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { + const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; + const { visualizationState } = props; + + return ( + <> + + {activeVisualization && visualizationState && ( + + )} + + ); +}); + +function LayerPanels( + props: ConfigPanelWrapperProps & { + activeDatasourceId: string; + activeVisualization: Visualization; + } +) { + const { + framePublicAPI, + activeVisualization, + visualizationState, + dispatch, + activeDatasourceId, + datasourceMap, + } = props; + const setVisualizationState = useMemo( + () => (newState: unknown) => { + props.dispatch({ + type: 'UPDATE_VISUALIZATION_STATE', + visualizationId: activeVisualization.id, + newState, + clearStagedPreview: false, + }); + }, + [props.dispatch, activeVisualization] + ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: false, + }); + }, + [props.dispatch] + ); + const updateAll = useMemo( + () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { + props.dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: prevState => { + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: newDatasourceState, + isLoading: false, + }, + }, + visualization: { + ...prevState.visualization, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [props.dispatch] + ); + const layerIds = activeVisualization.getLayerIds(visualizationState); + + return ( + + {layerIds.map(layerId => ( + { + dispatch({ + type: 'UPDATE_STATE', + subType: 'REMOVE_OR_CLEAR_LAYER', + updater: state => + removeLayer({ + activeVisualization, + layerId, + trackUiEvent, + datasourceMap, + state, + }), + }); + }} + /> + ))} + {activeVisualization.appendLayer && visualizationState && ( + + + { + dispatch({ + type: 'UPDATE_STATE', + subType: 'ADD_LAYER', + updater: state => + appendLayer({ + activeVisualization, + generateId, + trackUiEvent, + activeDatasource: datasourceMap[activeDatasourceId], + state, + }), + }); + }} + iconType="plusInCircleFilled" + /> + + + )} + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx new file mode 100644 index 0000000000000..36db13b74ac4f --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiPopover } from '@elastic/eui'; +import { VisualizationDimensionGroupConfig } from '../../../types'; +import { DimensionPopoverState } from './types'; + +export function DimensionPopover({ + popoverState, + setPopoverState, + groups, + accessor, + groupId, + trigger, + panel, +}: { + popoverState: DimensionPopoverState; + setPopoverState: (newState: DimensionPopoverState) => void; + groups: VisualizationDimensionGroupConfig[]; + accessor: string; + groupId: string; + trigger: React.ReactElement; + panel: React.ReactElement; +}) { + const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(accessor)) : false; + return ( + { + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + }} + button={trigger} + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + {panel} + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts new file mode 100644 index 0000000000000..754b3fb5c6fde --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ConfigPanelWrapper } from './config_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts similarity index 100% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts similarity index 95% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts index cc2cbb172d23e..3d1d590664238 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts @@ -5,8 +5,8 @@ */ import _ from 'lodash'; -import { EditorFrameState } from './state_management'; -import { Datasource, Visualization } from '../../types'; +import { EditorFrameState } from '../state_management'; +import { Datasource, Visualization } from '../../../types'; interface RemoveLayerOptions { trackUiEvent: (name: string) => void; 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 new file mode 100644 index 0000000000000..f7be82dd34ba3 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -0,0 +1,405 @@ +/* + * 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, { useContext, useState } from 'react'; +import { + EuiPanel, + EuiSpacer, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NativeRenderer } from '../../../native_renderer'; +import { Visualization, FramePublicAPI, StateSetter } from '../../../types'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop'; +import { LayerSettings } from './layer_settings'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { generateId } from '../../../id_generator'; +import { ConfigPanelWrapperProps, DimensionPopoverState } from './types'; +import { DimensionPopover } from './dimension_popover'; + +export function LayerPanel( + props: Exclude & { + frame: FramePublicAPI; + layerId: string; + isOnlyLayer: boolean; + activeVisualization: Visualization; + visualizationState: unknown; + updateVisualization: StateSetter; + updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; + onRemoveLayer: () => void; + } +) { + const dragDropContext = useContext(DragContext); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; + const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; + if (!datasourcePublicAPI) { + return null; + } + const layerVisualizationConfigProps = { + layerId, + dragDropContext, + state: props.visualizationState, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; + + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; + + const layerDatasourceConfigProps = { + ...layerDatasourceDropProps, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + + const [popoverState, setPopoverState] = useState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some(d => d.accessors.length > 0); + + return ( + + + + + + + + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + dateRange: props.framePublicAPI.dateRange, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter(columnId => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach(columnId => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); + }); + + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + + + + + {groups.map((group, index) => { + const newId = generateId(); + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + + <> + {group.accessors.map(accessor => ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); + }} + > + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + }); + } + }, + }} + /> + } + panel={ + + } + /> + + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + props.activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + + ))} + {group.supportsMoreColumns ? ( + { + const dropSuccess = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: group.filterOperations, + }); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } + }} + > + + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: group.groupId, + }); + } + }} + size="xs" + > + + +
+ } + panel={ + { + props.updateAll( + datasourceId, + newState, + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: null, // clear now that dimension exists + }); + }, + }} + /> + } + /> + + ) : null} + + + ); + })} + + + + + + { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: 'Delete layer', + })} + + + + + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx new file mode 100644 index 0000000000000..57588e31590b4 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx @@ -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 React, { useState } from 'react'; +import { EuiPopover, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { NativeRenderer } from '../../../native_renderer'; +import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; + +export function LayerSettings({ + layerId, + activeVisualization, + layerConfigProps, +}: { + layerId: string; + activeVisualization: Visualization; + layerConfigProps: VisualizationLayerWidgetProps; +}) { + const [isOpen, setIsOpen] = useState(false); + + if (!activeVisualization.renderLayerContextMenu) { + return null; + } + + return ( + setIsOpen(!isOpen)} + data-test-subj="lns_layer_settings" + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + anchorPosition="leftUp" + > + + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts new file mode 100644 index 0000000000000..df510d3648f8c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { Action } from '../state_management'; +import { + Visualization, + FramePublicAPI, + Datasource, + DatasourceDimensionEditorProps, +} from '../../../types'; + +export interface ConfigPanelWrapperProps { + activeDatasourceId: string; + visualizationState: unknown; + visualizationMap: Record; + activeVisualizationId: string | null; + dispatch: (action: Action) => void; + framePublicAPI: FramePublicAPI; + datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; + core: DatasourceDimensionEditorProps['core']; +} + +export interface DimensionPopoverState { + isOpen: boolean; + openId: string | null; + addingToGroupId: string | null; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx deleted file mode 100644 index da812e948b23f..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ /dev/null @@ -1,655 +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 React, { useMemo, useContext, memo, useState } from 'react'; -import { - EuiPanel, - EuiSpacer, - EuiPopover, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiToolTip, - EuiButton, - EuiForm, - EuiFormRow, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { NativeRenderer } from '../../native_renderer'; -import { Action } from './state_management'; -import { - Visualization, - FramePublicAPI, - Datasource, - VisualizationLayerWidgetProps, - DatasourceDimensionEditorProps, - StateSetter, -} from '../../types'; -import { DragContext, DragDrop, ChildDragDropProvider } from '../../drag_drop'; -import { ChartSwitch } from './chart_switch'; -import { trackUiEvent } from '../../lens_ui_telemetry'; -import { generateId } from '../../id_generator'; -import { removeLayer, appendLayer } from './layer_actions'; - -interface ConfigPanelWrapperProps { - activeDatasourceId: string; - visualizationState: unknown; - visualizationMap: Record; - activeVisualizationId: string | null; - dispatch: (action: Action) => void; - framePublicAPI: FramePublicAPI; - datasourceMap: Record; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; - core: DatasourceDimensionEditorProps['core']; -} - -export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { - const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; - const { visualizationState } = props; - - return ( - <> - - {activeVisualization && visualizationState && ( - - )} - - ); -}); - -function LayerPanels( - props: ConfigPanelWrapperProps & { - activeDatasourceId: string; - activeVisualization: Visualization; - } -) { - const { - framePublicAPI, - activeVisualization, - visualizationState, - dispatch, - activeDatasourceId, - datasourceMap, - } = props; - const setVisualizationState = useMemo( - () => (newState: unknown) => { - props.dispatch({ - type: 'UPDATE_VISUALIZATION_STATE', - visualizationId: activeVisualization.id, - newState, - clearStagedPreview: false, - }); - }, - [props.dispatch, activeVisualization] - ); - const updateDatasource = useMemo( - () => (datasourceId: string, newState: unknown) => { - props.dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: () => newState, - datasourceId, - clearStagedPreview: false, - }); - }, - [props.dispatch] - ); - const updateAll = useMemo( - () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { - props.dispatch({ - type: 'UPDATE_STATE', - subType: 'UPDATE_ALL_STATES', - updater: prevState => { - return { - ...prevState, - datasourceStates: { - ...prevState.datasourceStates, - [datasourceId]: { - state: newDatasourceState, - isLoading: false, - }, - }, - visualization: { - ...prevState.visualization, - state: newVisualizationState, - }, - stagedPreview: undefined, - }; - }, - }); - }, - [props.dispatch] - ); - const layerIds = activeVisualization.getLayerIds(visualizationState); - - return ( - - {layerIds.map(layerId => ( - { - dispatch({ - type: 'UPDATE_STATE', - subType: 'REMOVE_OR_CLEAR_LAYER', - updater: state => - removeLayer({ - activeVisualization, - layerId, - trackUiEvent, - datasourceMap, - state, - }), - }); - }} - /> - ))} - {activeVisualization.appendLayer && ( - - - { - dispatch({ - type: 'UPDATE_STATE', - subType: 'ADD_LAYER', - updater: state => - appendLayer({ - activeVisualization, - generateId, - trackUiEvent, - activeDatasource: datasourceMap[activeDatasourceId], - state, - }), - }); - }} - iconType="plusInCircleFilled" - /> - - - )} - - ); -} - -function LayerPanel( - props: Exclude & { - frame: FramePublicAPI; - layerId: string; - isOnlyLayer: boolean; - activeVisualization: Visualization; - visualizationState: unknown; - updateVisualization: StateSetter; - updateDatasource: (datasourceId: string, newState: unknown) => void; - updateAll: ( - datasourceId: string, - newDatasourcestate: unknown, - newVisualizationState: unknown - ) => void; - onRemoveLayer: () => void; - } -) { - const dragDropContext = useContext(DragContext); - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; - const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - if (!datasourcePublicAPI) { - return null; - } - const layerVisualizationConfigProps = { - layerId, - dragDropContext, - state: props.visualizationState, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - const datasourceId = datasourcePublicAPI.datasourceId; - const layerDatasourceState = props.datasourceStates[datasourceId].state; - const layerDatasource = props.datasourceMap[datasourceId]; - - const layerDatasourceDropProps = { - layerId, - dragDropContext, - state: layerDatasourceState, - setState: (newState: unknown) => { - props.updateDatasource(datasourceId, newState); - }, - }; - - const layerDatasourceConfigProps = { - ...layerDatasourceDropProps, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - - const [popoverState, setPopoverState] = useState<{ - isOpen: boolean; - openId: string | null; - addingToGroupId: string | null; - }>({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - - const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); - const isEmptyLayer = !groups.some(d => d.accessors.length > 0); - - function wrapInPopover( - id: string, - groupId: string, - trigger: React.ReactElement, - panel: React.ReactElement - ) { - const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false; - return ( - { - setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); - }} - button={trigger} - anchorPosition="leftUp" - withTitle - panelPaddingSize="s" - > - {panel} - - ); - } - - return ( - - - - - - - - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - dateRange: props.framePublicAPI.dateRange, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter(columnId => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach(columnId => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - - )} - - - - - {groups.map((group, index) => { - const newId = generateId(); - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - - <> - {group.accessors.map(accessor => ( - { - layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: accessor, - filterOperations: group.filterOperations, - }); - }} - > - {wrapInPopover( - accessor, - group.groupId, - { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: accessor, - addingToGroupId: null, // not set for existing dimension - }); - } - }, - }} - />, - - )} - - { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: accessor, - prevState: layerDatasourceState, - }), - props.activeVisualization.removeDimension({ - layerId, - columnId: accessor, - prevState: props.visualizationState, - }) - ); - }} - /> - - ))} - {group.supportsMoreColumns ? ( - { - const dropSuccess = layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: newId, - filterOperations: group.filterOperations, - }); - if (dropSuccess) { - props.updateVisualization( - activeVisualization.setDimension({ - layerId, - groupId: group.groupId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - } - }} - > - {wrapInPopover( - newId, - group.groupId, -
- { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: newId, - addingToGroupId: group.groupId, - }); - } - }} - size="xs" - > - - -
, - { - props.updateAll( - datasourceId, - newState, - activeVisualization.setDimension({ - layerId, - groupId: group.groupId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - setPopoverState({ - isOpen: true, - openId: newId, - addingToGroupId: null, // clear now that dimension exists - }); - }, - }} - /> - )} -
- ) : null} - -
- ); - })} - - - - - - { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; - - if (el?.blur) { - el.blur(); - } - - onRemoveLayer(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - - - -
-
- ); -} - -function LayerSettings({ - layerId, - activeVisualization, - layerConfigProps, -}: { - layerId: string; - activeVisualization: Visualization; - layerConfigProps: VisualizationLayerWidgetProps; -}) { - const [isOpen, setIsOpen] = useState(false); - - if (!activeVisualization.renderLayerContextMenu) { - return null; - } - - return ( - setIsOpen(!isOpen)} - data-test-subj="lns_layer_settings" - /> - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - anchorPosition="leftUp" - > - - - ); -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index b72d9081bbc91..5cd803e7cebbc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -16,7 +16,7 @@ import { } from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; -import { ConfigPanelWrapper } from './config_panel_wrapper'; +import { ConfigPanelWrapper } from './config_panel'; import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss index d4b27c6c98b3c..5e3726c953f11 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -1,8 +1,6 @@ -@import 'chart_switch'; -@import 'config_panel_wrapper'; +@import 'config_panel/index'; @import 'data_panel_wrapper'; @import 'expression_renderer'; @import 'frame_layout'; @import 'suggestion_panel'; @import 'workspace_panel_wrapper'; - 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 b3bd08d3bbfbe..583832aafcbe8 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 @@ -192,7 +192,7 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens return ( { props.togglePopover(); }} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 0b432c0c70727..181f192520d0d 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -273,7 +273,7 @@ export type VisualizationLayerWidgetProps = VisualizationConfigProp setState: (newState: T) => void; }; -type VisualizationDimensionGroupConfig = SharedDimensionProps & { +export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; /** ID is passed back to visualization. For example, `x` */ @@ -368,55 +368,86 @@ export interface VisualizationType { } export interface Visualization { + /** Plugin ID, such as "lnsXY" */ id: string; + /** + * Initialize is allowed to modify the state stored in memory. The initialize function + * is called with a previous state in two cases: + * - Loadingn from a saved visualization + * - When using suggestions, the suggested state is passed in + */ + initialize: (frame: FramePublicAPI, state?: P) => T; + /** + * Can remove any state that should not be persisted to saved object, such as UI state + */ + getPersistableState: (state: T) => P; + + /** + * Visualizations must provide at least one type for the chart switcher, + * but can register multiple subtypes + */ visualizationTypes: VisualizationType[]; + /** + * If the visualization has subtypes, update the subtype in state. + */ + switchVisualizationType?: (visualizationTypeId: string, state: T) => T; + /** Description is displayed as the clickable text in the chart switcher */ + getDescription: (state: T) => { icon?: IconType; label: string }; + /** Frame needs to know which layers the visualization is currently using */ getLayerIds: (state: T) => string[]; + /** Reset button on each layer triggers this */ clearLayer: (state: T, layerId: string) => T; + /** Optional, if the visualization supports multiple layers */ removeLayer?: (state: T, layerId: string) => T; + /** Track added layers in internal state */ appendLayer?: (state: T, layerId: string) => T; - // Layer context menu is used by visualizations for styling the entire layer - // For example, the XY visualization uses this to have multiple chart types - getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; - + /** + * For consistency across different visualizations, the dimension configuration UI is standardized + */ getConfiguration: ( props: VisualizationConfigProps ) => { groups: VisualizationDimensionGroupConfig[] }; - getDescription: ( - state: T - ) => { - icon?: IconType; - label: string; - }; - - switchVisualizationType?: (visualizationTypeId: string, state: T) => T; - - // For initializing from saved object - initialize: (frame: FramePublicAPI, state?: P) => T; - - getPersistableState: (state: T) => P; + /** + * Popover contents that open when the user clicks the contextMenuIcon. This can be used + * for extra configurability, such as for styling the legend or axis + */ + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; + /** + * Visualizations can provide a custom icon which will open a layer-specific popover + * If no icon is provided, gear icon is default + */ + getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; - // Actions triggered by the frame which tell the datasource that a dimension is being changed - setDimension: ( - props: VisualizationDimensionChangeProps & { - groupId: string; - } - ) => T; + /** + * The frame is telling the visualization to update or set a dimension based on user interaction + * groupId is coming from the groupId provided in getConfiguration + */ + setDimension: (props: VisualizationDimensionChangeProps & { groupId: string }) => T; + /** + * The frame is telling the visualization to remove a dimension. The visualization needs to + * look at its internal state to determine which dimension is being affected. + */ removeDimension: (props: VisualizationDimensionChangeProps) => T; - toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; + /** + * The frame will call this function on all visualizations at different times. The + * main use cases where visualization suggestions are requested are: + * - When dragging a field + * - When opening the chart switcher + * If the state is provided when requesting suggestions, the visualization is active. + * Most visualizations will apply stricter filtering to suggestions when they are active, + * because suggestions have the potential to remove the users's work in progress. + */ + getSuggestions: (context: SuggestionRequest) => Array>; + toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; /** - * Epression to render a preview version of the chart in very constraint space. + * Expression to render a preview version of the chart in very constrained space. * If there is no expression provided, the preview icon is used. */ toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; - - // The frame will call this function on all visualizations when the table changes, or when - // rendering additional ways of using the data - getSuggestions: (context: SuggestionRequest) => Array>; } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 80d33d1b95b61..e75e5fe763d6a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -439,7 +439,7 @@ describe('xy_expression', () => { }); test('onElementClick returns correct context data', () => { - const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1' }; + const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null }; const series = { key: 'spec{d}yAccessor{d}splitAccessors{b-2}', specId: 'd', diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index f12a0e5b907c7..d6b6de479acfb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -28,7 +28,7 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EmbeddableVisTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; import { LensMultiTable, FormatFactory } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; @@ -277,7 +277,7 @@ export function XYChart({ const timeFieldName = xDomain && xAxisFieldName; - const context: EmbeddableVisTriggerContext = { + const context: ValueClickTriggerContext = { data: { data: points.map(point => ({ row: point.row, diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json index bcc926535d3c2..97dbf58865a88 100644 --- a/x-pack/plugins/logstash/kibana.json +++ b/x-pack/plugins/logstash/kibana.json @@ -4,9 +4,13 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "logstash"], "requiredPlugins": [ - "licensing" + "licensing", + "management" + ], + "optionalPlugins": [ + "home", + "security" ], - "optionalPlugins": ["security"], "server": true, - "ui": false + "ui": true } diff --git a/x-pack/legacy/plugins/logstash/public/sections/breadcrumbs.js b/x-pack/plugins/logstash/public/application/breadcrumbs.js similarity index 80% rename from x-pack/legacy/plugins/logstash/public/sections/breadcrumbs.js rename to x-pack/plugins/logstash/public/application/breadcrumbs.js index 3121a58ff6a74..322b9860b3785 100644 --- a/x-pack/legacy/plugins/logstash/public/sections/breadcrumbs.js +++ b/x-pack/plugins/logstash/public/application/breadcrumbs.js @@ -5,11 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; export function getPipelineListBreadcrumbs() { return [ - MANAGEMENT_BREADCRUMB, { text: i18n.translate('xpack.logstash.pipelines.listBreadcrumb', { defaultMessage: 'Pipelines', @@ -19,12 +17,11 @@ export function getPipelineListBreadcrumbs() { ]; } -export function getPipelineEditBreadcrumbs($route) { - const { pipeline } = $route.current.locals; +export function getPipelineEditBreadcrumbs(pipelineId) { return [ ...getPipelineListBreadcrumbs(), { - text: pipeline.id, + text: pipelineId, }, ]; } diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/constants.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/constants.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/constants.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/constants.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/index.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/index.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/index.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js similarity index 97% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js index 5e430ccbd8ceb..e45820d56cc03 100644 --- a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js @@ -13,7 +13,7 @@ import 'brace/mode/plain_text'; import 'brace/theme/github'; import { isEmpty } from 'lodash'; -import { TOOLTIPS } from '../../../../../../plugins/logstash/common/constants/tooltips'; +import { TOOLTIPS } from '../../../../common/constants/tooltips'; import { EuiButton, EuiButtonEmpty, @@ -40,7 +40,6 @@ class PipelineEditorUi extends React.Component { const { pipeline: { id, description, pipeline, settings }, - username, } = this.props; const pipelineWorkersSet = typeof settings['pipeline.workers'] === 'number'; @@ -60,7 +59,6 @@ class PipelineEditorUi extends React.Component { 'queue.max_bytes': settings['queue.max_bytes.number'] + settings['queue.max_bytes.units'], 'queue.type': settings['queue.type'], }, - username, }, pipelineIdErrors: [], pipelineIdPattern: /^[A-Za-z\_][A-Za-z0-9\-\_]*$/, @@ -236,15 +234,7 @@ class PipelineEditorUi extends React.Component { }; getPipelineHeadingText = () => { - const { - routeService: { - current: { - params: { clone, id }, - }, - }, - isNewPipeline, - intl, - } = this.props; + const { clone, id, isNewPipeline, intl } = this.props; if (!!clone && id) { return intl.formatMessage( @@ -502,6 +492,8 @@ class PipelineEditorUi extends React.Component { } PipelineEditorUi.propTypes = { + id: PropTypes.string, + clone: PropTypes.bool.isRequired, close: PropTypes.func.isRequired, isNewPipeline: PropTypes.bool.isRequired, licenseService: PropTypes.shape({ @@ -527,20 +519,11 @@ PipelineEditorUi.propTypes = { deletePipeline: PropTypes.func.isRequired, savePipeline: PropTypes.func.isRequired, }).isRequired, - routeService: PropTypes.shape({ - current: PropTypes.shape({ - params: PropTypes.shape({ - clone: PropTypes.oneOf([true, undefined]), - id: PropTypes.string, - }), - }), - }).isRequired, toastNotifications: PropTypes.shape({ addWarning: PropTypes.func.isRequired, addSuccess: PropTypes.func.isRequired, addError: PropTypes.func.isRequired, }).isRequired, - username: PropTypes.string, }; export const PipelineEditor = injectI18n(PipelineEditorUi); diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.test.js similarity index 96% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.test.js index 2d7ed5f257fbd..bb5961ce36120 100644 --- a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.test.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.test.js @@ -17,7 +17,6 @@ describe('PipelineEditor component', () => { let open; let pipeline; let pipelineService; - let routeService; let toastNotifications; let username; @@ -47,14 +46,6 @@ describe('PipelineEditor component', () => { deletePipeline: jest.fn(), savePipeline: jest.fn(), }; - routeService = { - current: { - params: { - clone: undefined, - id: undefined, - }, - }, - }; toastNotifications = { addWarning: jest.fn(), addSuccess: jest.fn(), @@ -62,13 +53,14 @@ describe('PipelineEditor component', () => { }; username = 'elastic'; props = { + clone: false, + id: 'pipelineId', close, isNewPipeline, licenseService, open, pipeline, pipelineService, - routeService, toastNotifications, username, }; @@ -79,10 +71,8 @@ describe('PipelineEditor component', () => { }); it('matches snapshot for clone pipeline', () => { - routeService.current.params = { - clone: true, - id: 'pipelineToClone', - }; + props.clone = true; + props.id = 'pipelineToClone'; expect(shallowWithIntl()).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/info_alerts.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/info_alerts.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/info_alerts.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/info_alerts.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/constants.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/constants.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/constants.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/constants.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/index.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/index.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/index.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/constants.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/constants.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/constants.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/constants.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/index.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/index.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/index.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.test.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.test.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.test.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.test.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.test.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.test.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.test.js diff --git a/x-pack/plugins/logstash/public/application/index.tsx b/x-pack/plugins/logstash/public/application/index.tsx new file mode 100644 index 0000000000000..438038d6c885e --- /dev/null +++ b/x-pack/plugins/logstash/public/application/index.tsx @@ -0,0 +1,117 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; + +import { CoreStart } from 'src/core/public'; +import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; +import { + ClusterService, + MonitoringService, + PipelineService, + PipelinesService, + UpgradeService, + // @ts-ignore +} from '../services'; +// @ts-ignore +import { PipelineList } from './components/pipeline_list'; +import { PipelineEditView } from './pipeline_edit_view'; +// @ts-ignore +import { Pipeline } from '../models/pipeline'; +// @ts-ignore +import * as Breadcrumbs from './breadcrumbs'; + +export const renderApp = async ( + core: CoreStart, + { basePath, element, setBreadcrumbs }: ManagementAppMountParams, + licenseService$: Observable +) => { + const logstashLicenseService = await licenseService$.pipe(first()).toPromise(); + const clusterService = new ClusterService(core.http); + const monitoringService = new MonitoringService( + core.http, + // When monitoring is migrated this should be fetched from monitoring's plugin contract + core.injectedMetadata.getInjectedVar('monitoringUiEnabled'), + clusterService + ); + const pipelinesService = new PipelinesService(core.http, monitoringService); + const pipelineService = new PipelineService(core.http, pipelinesService); + const upgradeService = new UpgradeService(core.http); + + ReactDOM.render( + + + + { + setBreadcrumbs(Breadcrumbs.getPipelineListBreadcrumbs()); + return ( + history.push(`/pipeline/${id}/edit`)} + clonePipeline={(id: string) => history.push(`/pipeline/${id}/edit?clone`)} + createPipeline={() => history.push(`/pipeline/new-pipeline`)} + pipelinesService={pipelinesService} + toastNotifications={core.notifications.toasts} + /> + ); + }} + /> + ( + + )} + /> + } + /> + ( + + )} + /> + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx b/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx new file mode 100644 index 0000000000000..c1b465febcd9b --- /dev/null +++ b/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx @@ -0,0 +1,153 @@ +/* + * 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, { useState, useLayoutEffect, useCallback } from 'react'; +import { usePromise } from 'react-use'; +import { History } from 'history'; + +import { i18n } from '@kbn/i18n'; +import { ToastsStart } from 'src/core/public'; + +// @ts-ignore +import { UpgradeFailure } from './components/upgrade_failure'; +// @ts-ignore +import { PipelineEditor } from './components/pipeline_editor'; +// @ts-ignore +import { Pipeline } from '../models/pipeline'; +import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; +// @ts-ignore +import * as Breadcrumbs from './breadcrumbs'; + +const usePipeline = ( + pipelineService: any, + logstashLicenseService: any, + toasts: ToastsStart, + shouldClone: boolean, + id?: string +) => { + const mounted = usePromise(); + const [pipeline, setPipeline] = useState(null); + + useLayoutEffect(() => { + (async () => { + if (!id) { + return setPipeline(new Pipeline()); + } + + try { + const result = await mounted(pipelineService.loadPipeline(id) as Promise); + setPipeline(shouldClone ? result.clone : result); + } catch (e) { + await logstashLicenseService.checkValidity(); + if (e.status !== 403) { + toasts.addDanger( + i18n.translate('xpack.logstash.couldNotLoadPipelineErrorNotification', { + defaultMessage: `Couldn't load pipeline. Error: '{errStatusText}'.`, + values: { + errStatusText: e.statusText, + }, + }) + ); + } + } + })(); + }, [pipelineService, id, mounted, shouldClone, logstashLicenseService, toasts]); + + return pipeline; +}; + +const useIsUpgraded = (upgradeService: any) => { + const [isUpgraded, setIsUpgraded] = useState(null); + const mounted = usePromise(); + + useLayoutEffect(() => { + mounted(upgradeService.executeUpgrade() as Promise).then(result => + setIsUpgraded(result) + ); + }, [mounted, upgradeService]); + + return isUpgraded; +}; + +interface EditProps { + pipelineService: any; + logstashLicenseService: any; + upgradeService: any; + toasts: ToastsStart; + history: History; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; + + // URL params + id?: string; +} + +export const PipelineEditView: React.FC = ({ + pipelineService, + logstashLicenseService, + upgradeService, + toasts, + history, + setBreadcrumbs, + id, +}) => { + const params = new URLSearchParams(history.location.search); + const shouldRetry = params.get('retry') === 'true'; + const shouldClone = params.get('clone') === ''; + + const pipeline = usePipeline(pipelineService, logstashLicenseService, toasts, shouldClone, id); + const isUpgraded = useIsUpgraded(upgradeService); + + const onRetry = useCallback(() => { + const newParams = new URLSearchParams(history.location.search); + newParams.set('retry', 'true'); + history.replace({ search: newParams.toString() }); + }, [history]); + const close = useCallback(() => { + history.push('/'); + }, [history]); + const open = useCallback( + (newId: string) => { + history.push(`/pipeline/${newId}/edit`); + }, + [history] + ); + + if (!pipeline || isUpgraded === null) { + return null; + } + + const isNewPipeline = !pipeline.id; + setBreadcrumbs( + isNewPipeline + ? Breadcrumbs.getPipelineCreateBreadcrumbs() + : Breadcrumbs.getPipelineEditBreadcrumbs(pipeline.id) + ); + + if (!isUpgraded) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/logstash/public/index.ts b/x-pack/plugins/logstash/public/index.ts new file mode 100644 index 0000000000000..26a1ca4e8c6c4 --- /dev/null +++ b/x-pack/plugins/logstash/public/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogstashPlugin } from './plugin'; + +export const plugin = () => new LogstashPlugin(); diff --git a/x-pack/legacy/plugins/logstash/public/lib/get_search_value/get_search_value.js b/x-pack/plugins/logstash/public/lib/get_search_value/get_search_value.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/lib/get_search_value/get_search_value.js rename to x-pack/plugins/logstash/public/lib/get_search_value/get_search_value.js diff --git a/x-pack/legacy/plugins/logstash/public/lib/get_search_value/index.js b/x-pack/plugins/logstash/public/lib/get_search_value/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/lib/get_search_value/index.js rename to x-pack/plugins/logstash/public/lib/get_search_value/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/cluster/cluster.js b/x-pack/plugins/logstash/public/models/cluster/cluster.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/cluster/cluster.js rename to x-pack/plugins/logstash/public/models/cluster/cluster.js diff --git a/x-pack/legacy/plugins/logstash/public/models/cluster/index.js b/x-pack/plugins/logstash/public/models/cluster/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/cluster/index.js rename to x-pack/plugins/logstash/public/models/cluster/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline/index.js b/x-pack/plugins/logstash/public/models/pipeline/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/pipeline/index.js rename to x-pack/plugins/logstash/public/models/pipeline/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline/pipeline.js b/x-pack/plugins/logstash/public/models/pipeline/pipeline.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/pipeline/pipeline.js rename to x-pack/plugins/logstash/public/models/pipeline/pipeline.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/index.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/index.js rename to x-pack/plugins/logstash/public/models/pipeline_list_item/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js similarity index 83% rename from x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js rename to x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js index 06d01a05bac27..3a304e467e0c0 100755 --- a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js +++ b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js @@ -5,10 +5,10 @@ */ import { pick, capitalize } from 'lodash'; +import moment from 'moment'; -import { getSearchValue } from 'plugins/logstash/lib/get_search_value'; -import { getMoment } from 'plugins/logstash/../common/lib/get_moment'; -import { PIPELINE } from '../../../../../../plugins/logstash/common/constants'; +import { getSearchValue } from '../../lib/get_search_value'; +import { PIPELINE } from '../../../common/constants'; /** * Represents the model for listing pipelines in the UI @@ -25,7 +25,7 @@ export class PipelineListItem { this.username = props.username; if (props.lastModified) { - this.lastModified = getMoment(props.lastModified); + this.lastModified = getMomentDate(props.lastModified); this.lastModifiedHumanized = capitalize(this.lastModified.fromNow()); } } @@ -51,3 +51,11 @@ export class PipelineListItem { return new PipelineListItem(props); } } + +function getMomentDate(date) { + if (!date) { + return null; + } + + return moment(date); +} diff --git a/x-pack/plugins/logstash/public/plugin.ts b/x-pack/plugins/logstash/public/plugin.ts new file mode 100644 index 0000000000000..91d1a39d3970c --- /dev/null +++ b/x-pack/plugins/logstash/public/plugin.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 { i18n } from '@kbn/i18n'; +import { Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { once } from 'lodash'; + +import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { + HomePublicPluginSetup, + FeatureCatalogueCategory, +} from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; + +// @ts-ignore +import { LogstashLicenseService } from './services'; + +interface SetupDeps { + licensing: LicensingPluginSetup; + management: ManagementSetup; + + home?: HomePublicPluginSetup; +} + +export class LogstashPlugin implements Plugin { + private licenseSubscription?: Subscription; + + public setup(core: CoreSetup, plugins: SetupDeps) { + const logstashLicense$ = plugins.licensing.license$.pipe( + map(license => new LogstashLicenseService(license)) + ); + const section = plugins.management.sections.register({ + id: 'logstash', + title: 'Logstash', + order: 30, + euiIconType: 'logoLogstash', + }); + const managementApp = section.registerApp({ + id: 'pipelines', + title: i18n.translate('xpack.logstash.managementSection.pipelinesTitle', { + defaultMessage: 'Pipelines', + }), + order: 10, + mount: async params => { + const [coreStart] = await core.getStartServices(); + const { renderApp } = await import('./application'); + + return renderApp(coreStart, params, logstashLicense$); + }, + }); + + this.licenseSubscription = logstashLicense$.subscribe((license: any) => { + if (license.enableLinks) { + managementApp.enable(); + } else { + managementApp.disable(); + } + + if (plugins.home && license.enableLinks) { + // Ensure that we don't register the feature more than once + once(() => { + plugins.home!.featureCatalogue.register({ + id: 'management_logstash', + title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { + defaultMessage: 'Logstash Pipelines', + }), + description: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesDescription', { + defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', + }), + icon: 'pipelineApp', + path: '/app/kibana#/management/logstash/pipelines', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + }); + } + }); + } + + public start(core: CoreStart) {} + + public stop() { + if (this.licenseSubscription) { + this.licenseSubscription.unsubscribe(); + } + } +} diff --git a/x-pack/plugins/logstash/public/services/cluster/cluster_service.js b/x-pack/plugins/logstash/public/services/cluster/cluster_service.js new file mode 100755 index 0000000000000..20f3b0d349c80 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/cluster/cluster_service.js @@ -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. + */ + +import { ROUTES } from '../../../common/constants'; +import { Cluster } from '../../models/cluster'; + +export class ClusterService { + constructor(http) { + this.http = http; + } + + loadCluster() { + return this.http.get(`${ROUTES.API_ROOT}/cluster`).then(response => { + if (!response) { + return; + } + return Cluster.fromUpstreamJSON(response.cluster); + }); + } + + isClusterInfoAvailable() { + return this.loadCluster() + .then(cluster => Boolean(cluster)) + .catch(() => false); + } +} diff --git a/x-pack/plugins/logstash/public/services/cluster/index.js b/x-pack/plugins/logstash/public/services/cluster/index.js new file mode 100755 index 0000000000000..4417262d9f442 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/cluster/index.js @@ -0,0 +1,7 @@ +/* + * 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 { ClusterService } from './cluster_service'; diff --git a/x-pack/plugins/logstash/public/services/index.js b/x-pack/plugins/logstash/public/services/index.js new file mode 100644 index 0000000000000..a7e8aa5c6259f --- /dev/null +++ b/x-pack/plugins/logstash/public/services/index.js @@ -0,0 +1,12 @@ +/* + * 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 { ClusterService } from './cluster'; +export { LogstashLicenseService } from './license'; +export { MonitoringService } from './monitoring'; +export { PipelineService } from './pipeline'; +export { PipelinesService } from './pipelines'; +export { UpgradeService } from './upgrade'; diff --git a/x-pack/plugins/logstash/public/services/license/index.js b/x-pack/plugins/logstash/public/services/license/index.js new file mode 100755 index 0000000000000..64f39b1144cee --- /dev/null +++ b/x-pack/plugins/logstash/public/services/license/index.js @@ -0,0 +1,7 @@ +/* + * 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 { LogstashLicenseService } from './logstash_license_service'; diff --git a/x-pack/plugins/logstash/public/services/license/logstash_license_service.js b/x-pack/plugins/logstash/public/services/license/logstash_license_service.js new file mode 100755 index 0000000000000..b836b75b89cc7 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/license/logstash_license_service.js @@ -0,0 +1,106 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export class LogstashLicenseService { + constructor(license, navigateToApp, toasts) { + this.license = license; + this.navigateToApp = navigateToApp; + this.toasts = toasts; + } + + get enableLinks() { + return this.calculated.enableLinks; + } + + get isAvailable() { + return this.calculated.isAvailable; + } + + get isReadOnly() { + return this.calculated.isReadOnly; + } + + get message() { + return this.calculated.message; + } + + get isSecurityEnabled() { + return this.license.getFeature(`security`).isEnabled; + } + + /** + * Checks if the license is valid or the license can perform downgraded UI tasks. + * Rejects if the plugin is not available due to license. + */ + checkValidity() { + return new Promise((resolve, reject) => { + if (this.isAvailable) { + return resolve(); + } + + return reject(); + }); + } + + get calculated() { + if (!this.license) { + throw new Error(`No license available!`); + } + + if (!this.isSecurityEnabled) { + return { + isAvailable: false, + enableLinks: false, + isReadOnly: false, + message: i18n.translate('xpack.logstash.managementSection.enableSecurityDescription', { + defaultMessage: + 'Security must be enabled in order to use Logstash pipeline management features.' + + ' Please set xpack.security.enabled: true in your elasticsearch.yml.', + }), + }; + } + + if (!this.license.hasAtLeast('standard')) { + return { + isAvailable: false, + enableLinks: false, + isReadOnly: false, + message: i18n.translate( + 'xpack.logstash.managementSection.licenseDoesNotSupportDescription', + { + defaultMessage: + 'Your {licenseType} license does not support Logstash pipeline management features. Please upgrade your license.', + values: { licenseType: this.license.type }, + } + ), + }; + } + + if (!this.license.isActive) { + return { + isAvailable: true, + enableLinks: true, + isReadonly: true, + message: i18n.translate( + 'xpack.logstash.managementSection.pipelineCrudOperationsNotAllowedDescription', + { + defaultMessage: + 'You cannot edit, create, or delete your Logstash pipelines because your {licenseType} license has expired.', + values: { licenseType: this.license.type }, + } + ), + }; + } + + return { + isAvailable: true, + enableLinks: true, + isReadOnly: false, + }; + } +} diff --git a/x-pack/plugins/logstash/public/services/monitoring/index.js b/x-pack/plugins/logstash/public/services/monitoring/index.js new file mode 100755 index 0000000000000..bc0e8b6bc978a --- /dev/null +++ b/x-pack/plugins/logstash/public/services/monitoring/index.js @@ -0,0 +1,7 @@ +/* + * 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 { MonitoringService } from './monitoring_service'; diff --git a/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js new file mode 100755 index 0000000000000..d551f4fba61d2 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js @@ -0,0 +1,45 @@ +/* + * 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 moment from 'moment'; +import { ROUTES, MONITORING } from '../../../common/constants'; +import { PipelineListItem } from '../../models/pipeline_list_item'; + +export class MonitoringService { + constructor(http, monitoringUiEnabled, clusterService) { + this.http = http; + this.monitoringUiEnabled = monitoringUiEnabled; + this.clusterService = clusterService; + } + + isMonitoringEnabled() { + return this.monitoringUiEnabled; + } + + getPipelineList() { + if (!this.isMonitoringEnabled()) { + return Promise.resolve([]); + } + + return this.clusterService + .loadCluster() + .then(cluster => { + const url = `${ROUTES.MONITORING_API_ROOT}/v1/clusters/${cluster.uuid}/logstash/pipeline_ids`; + const now = moment.utc(); + const body = JSON.stringify({ + timeRange: { + max: now.toISOString(), + min: now.subtract(MONITORING.ACTIVE_PIPELINE_RANGE_S, 'seconds').toISOString(), + }, + }); + return this.http.post(url, { body }); + }) + .then(response => + response.map(pipeline => PipelineListItem.fromUpstreamMonitoringJSON(pipeline)) + ) + .catch(() => []); + } +} diff --git a/x-pack/plugins/logstash/public/services/pipeline/index.js b/x-pack/plugins/logstash/public/services/pipeline/index.js new file mode 100755 index 0000000000000..70d228b34860b --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipeline/index.js @@ -0,0 +1,7 @@ +/* + * 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 { PipelineService } from './pipeline_service'; diff --git a/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js b/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js new file mode 100755 index 0000000000000..7c3e18e745d82 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js @@ -0,0 +1,40 @@ +/* + * 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 { ROUTES } from '../../../common/constants'; +import { Pipeline } from '../../models/pipeline'; + +export class PipelineService { + constructor(http, pipelinesService) { + this.http = http; + this.pipelinesService = pipelinesService; + } + + loadPipeline(id) { + return this.http.get(`${ROUTES.API_ROOT}/pipeline/${id}`).then(response => { + return Pipeline.fromUpstreamJSON(response); + }); + } + + savePipeline(pipelineModel) { + return this.http + .put(`${ROUTES.API_ROOT}/pipeline/${pipelineModel.id}`, { + body: JSON.stringify(pipelineModel.upstreamJSON), + }) + .catch(e => { + throw e.message; + }); + } + + deletePipeline(id) { + return this.http + .delete(`${ROUTES.API_ROOT}/pipeline/${id}`) + .then(() => this.pipelinesService.addToRecentlyDeleted(id)) + .catch(e => { + throw e.message; + }); + } +} diff --git a/x-pack/plugins/logstash/public/services/pipelines/index.js b/x-pack/plugins/logstash/public/services/pipelines/index.js new file mode 100755 index 0000000000000..a932dd4b951f4 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipelines/index.js @@ -0,0 +1,7 @@ +/* + * 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 { PipelinesService } from './pipelines_service'; diff --git a/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js b/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js new file mode 100755 index 0000000000000..00610a23f2717 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js @@ -0,0 +1,128 @@ +/* + * 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 { ROUTES, MONITORING } from '../../../common/constants'; +import { PipelineListItem } from '../../models/pipeline_list_item'; + +const RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY = 'xpack.logstash.recentlyDeletedPipelines'; + +export class PipelinesService { + constructor(http, monitoringService) { + this.http = http; + this.monitoringService = monitoringService; + } + + getPipelineList() { + return Promise.all([this.getManagementPipelineList(), this.getMonitoringPipelineList()]).then( + ([managementPipelines, monitoringPipelines]) => { + const now = Date.now(); + + // Monitoring will report centrally-managed pipelines as well, including recently-deleted centrally-managed ones. + // If there's a recently-deleted pipeline we're keeping track of BUT monitoring doesn't report it, that means + // it's not running in Logstash any more. So we can stop tracking it as a recently-deleted pipeline. + const monitoringPipelineIds = monitoringPipelines.map(pipeline => pipeline.id); + this.getRecentlyDeleted().forEach(recentlyDeletedPipeline => { + // We don't want to stop tracking the recently-deleted pipeline until Monitoring has had some + // time to report on it. Otherwise, if we stop tracking first, *then* Monitoring reports it, we'll + // still end up showing it in the list until Monitoring stops reporting it. + if (now - recentlyDeletedPipeline.deletedOn < MONITORING.ACTIVE_PIPELINE_RANGE_S * 1000) { + return; + } + + // If Monitoring is still reporting the pipeline, don't stop tracking it yet + if (monitoringPipelineIds.includes(recentlyDeletedPipeline.id)) { + return; + } + + this.removeFromRecentlyDeleted(recentlyDeletedPipeline.id); + }); + + // Merge centrally-managed pipelines with pipelines reported by monitoring. Take care to dedupe + // while merging because monitoring will (rightly) report centrally-managed pipelines as well, + // including recently-deleted ones! + const managementPipelineIds = managementPipelines.map(pipeline => pipeline.id); + return managementPipelines.concat( + monitoringPipelines.filter( + monitoringPipeline => + !managementPipelineIds.includes(monitoringPipeline.id) && + !this.isRecentlyDeleted(monitoringPipeline.id) + ) + ); + } + ); + } + + getManagementPipelineList() { + return this.http.get(`${ROUTES.API_ROOT}/pipelines`).then(response => { + return response.pipelines.map(pipeline => PipelineListItem.fromUpstreamJSON(pipeline)); + }); + } + + getMonitoringPipelineList() { + return this.monitoringService.getPipelineList(); + } + + /** + * Delete a collection of pipelines + * + * @param pipelineIds Array of pipeline IDs + * @return Promise { numSuccesses, numErrors } + */ + deletePipelines(pipelineIds) { + const body = JSON.stringify({ + pipelineIds, + }); + return this.http.post(`${ROUTES.API_ROOT}/pipelines/delete`, { body }).then(response => { + this.addToRecentlyDeleted(...pipelineIds); + return response.results; + }); + } + + addToRecentlyDeleted(...pipelineIds) { + const recentlyDeletedPipelines = this.getRecentlyDeleted(); + const recentlyDeletedPipelineIds = recentlyDeletedPipelines.map(pipeline => pipeline.id); + pipelineIds.forEach(pipelineId => { + if (!recentlyDeletedPipelineIds.includes(pipelineId)) { + recentlyDeletedPipelines.push({ + id: pipelineId, + deletedOn: Date.now(), + }); + } + }); + this.setRecentlyDeleted(recentlyDeletedPipelines); + } + + removeFromRecentlyDeleted(...pipelineIds) { + const recentlyDeletedPipelinesToKeep = this.getRecentlyDeleted().filter( + recentlyDeletedPipeline => !pipelineIds.includes(recentlyDeletedPipeline.id) + ); + this.setRecentlyDeleted(recentlyDeletedPipelinesToKeep); + } + + isRecentlyDeleted(pipelineId) { + return this.getRecentlyDeleted() + .map(pipeline => pipeline.id) + .includes(pipelineId); + } + + getRecentlyDeleted() { + const recentlyDeletedPipelines = window.localStorage.getItem( + RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY + ); + if (!recentlyDeletedPipelines) { + return []; + } + + return JSON.parse(recentlyDeletedPipelines); + } + + setRecentlyDeleted(recentlyDeletedPipelineIds) { + window.localStorage.setItem( + RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY, + JSON.stringify(recentlyDeletedPipelineIds) + ); + } +} diff --git a/x-pack/plugins/logstash/public/services/upgrade/index.js b/x-pack/plugins/logstash/public/services/upgrade/index.js new file mode 100755 index 0000000000000..1c835b11ae423 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/upgrade/index.js @@ -0,0 +1,7 @@ +/* + * 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 { UpgradeService } from './upgrade_service'; diff --git a/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js b/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js new file mode 100755 index 0000000000000..7bd101ebee6b0 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js @@ -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 { ROUTES } from '../../../common/constants'; + +export class UpgradeService { + constructor(http) { + this.http = http; + } + + executeUpgrade() { + return this.http + .post(`${ROUTES.API_ROOT}/upgrade`) + .then(response => response.is_upgraded) + .catch(e => { + throw e.message; + }); + } +} diff --git a/x-pack/plugins/logstash/server/routes/pipeline/save.ts b/x-pack/plugins/logstash/server/routes/pipeline/save.ts index 556c281944a85..e484d0e221b6d 100644 --- a/x-pack/plugins/logstash/server/routes/pipeline/save.ts +++ b/x-pack/plugins/logstash/server/routes/pipeline/save.ts @@ -25,7 +25,6 @@ export function registerPipelineSaveRoute(router: IRouter, security?: SecurityPl id: schema.string(), description: schema.string(), pipeline: schema.string(), - username: schema.string(), settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index a4006732224ce..fd972219563a8 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -166,6 +166,7 @@ export enum STYLE_TYPE { export enum LAYER_STYLE_TYPE { VECTOR = 'VECTOR', HEATMAP = 'HEATMAP', + TILE = 'TILE', } export const COLOR_MAP_TYPE = { @@ -214,3 +215,5 @@ export enum SCALING_TYPES { } export const RGBA_0000 = 'rgba(0,0,0,0)'; + +export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID'; diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index f8175b0ed3f10..6980f14d0788a 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -5,8 +5,9 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { Query } from 'src/plugins/data/public'; import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; -import { VectorStyleDescriptor } from './style_property_descriptor_types'; +import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; export type AttributionDescriptor = { @@ -17,6 +18,7 @@ export type AttributionDescriptor = { export type AbstractSourceDescriptor = { id?: string; type: string; + applyGlobalQuery?: boolean; }; export type EMSTMSSourceDescriptor = AbstractSourceDescriptor & { @@ -71,17 +73,15 @@ export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & { term: string; // term field name }; -export type KibanaRegionmapSourceDescriptor = { - type: string; +export type KibanaRegionmapSourceDescriptor = AbstractSourceDescriptor & { name: string; }; -export type KibanaTilemapSourceDescriptor = { - type: string; -}; +// This is for symmetry with other sources only. +// It takes no additional configuration since all params are in the .yml. +export type KibanaTilemapSourceDescriptor = AbstractSourceDescriptor; -export type WMSSourceDescriptor = { - type: string; +export type WMSSourceDescriptor = AbstractSourceDescriptor & { serviceUrl: string; layers: string; styles: string; @@ -111,6 +111,8 @@ export type JoinDescriptor = { right: ESTermSourceDescriptor; }; +// todo : this union type is incompatible with dynamic extensibility of sources. +// Reconsider using SourceDescriptor in type signatures for top-level classes export type SourceDescriptor = | XYZTMSSourceDescriptor | WMSSourceDescriptor @@ -121,7 +123,9 @@ export type SourceDescriptor = | ESGeoGridSourceDescriptor | EMSFileSourceDescriptor | ESPewPewSourceDescriptor - | TiledSingleLayerVectorSourceDescriptor; + | TiledSingleLayerVectorSourceDescriptor + | EMSTMSSourceDescriptor + | EMSFileSourceDescriptor; export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; @@ -129,12 +133,14 @@ export type LayerDescriptor = { __errorMessage?: string; alpha?: number; id: string; - label?: string; + label?: string | null; minZoom?: number; maxZoom?: number; - sourceDescriptor: SourceDescriptor; + sourceDescriptor: SourceDescriptor | null; type?: string; visible?: boolean; + style?: StyleDescriptor | null; + query?: Query; }; export type VectorLayerDescriptor = LayerDescriptor & { diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts index 47e56ff96d623..381bc5bba01c0 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts @@ -182,7 +182,11 @@ export type VectorStylePropertiesDescriptor = { [VECTOR_STYLES.LABEL_BORDER_SIZE]?: LabelBorderSizeStylePropertyDescriptor; }; -export type VectorStyleDescriptor = { +export type StyleDescriptor = { + type: string; +}; + +export type VectorStyleDescriptor = StyleDescriptor & { type: LAYER_STYLE_TYPE.VECTOR; properties: VectorStylePropertiesDescriptor; }; diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js rename to x-pack/plugins/maps/common/migrations/add_field_meta_options.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js rename to x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js rename to x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js rename to x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.test.ts similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts rename to x-pack/plugins/maps/common/migrations/join_agg_key.test.ts diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.ts similarity index 97% rename from x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts rename to x-pack/plugins/maps/common/migrations/join_agg_key.ts index 29661aedb550c..97b9ee4692c25 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.ts @@ -20,7 +20,7 @@ import { LayerDescriptor, VectorLayerDescriptor, } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; const GROUP_BY_DELIMITER = '_groupby_'; diff --git a/x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js rename to x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js rename to x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js b/x-pack/plugins/maps/common/migrations/move_apply_global_query.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js rename to x-pack/plugins/maps/common/migrations/move_apply_global_query.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.test.js b/x-pack/plugins/maps/common/migrations/move_apply_global_query.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.test.js rename to x-pack/plugins/maps/common/migrations/move_apply_global_query.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/references.js b/x-pack/plugins/maps/common/migrations/references.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/references.js rename to x-pack/plugins/maps/common/migrations/references.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/references.test.js b/x-pack/plugins/maps/common/migrations/references.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/references.test.js rename to x-pack/plugins/maps/common/migrations/references.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts b/x-pack/plugins/maps/common/migrations/scaling_type.test.ts similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts rename to x-pack/plugins/maps/common/migrations/scaling_type.test.ts diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts b/x-pack/plugins/maps/common/migrations/scaling_type.ts similarity index 93% rename from x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts rename to x-pack/plugins/maps/common/migrations/scaling_type.ts index 551975fbacea5..98a06a764f4ec 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts +++ b/x-pack/plugins/maps/common/migrations/scaling_type.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { SOURCE_TYPES, SCALING_TYPES } from '../constants'; import { LayerDescriptor, ESSearchSourceDescriptor } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; function isEsDocumentSource(layerDescriptor: LayerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); diff --git a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js rename to x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.test.js b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.test.js rename to x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.test.js diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index b2aec30c113eb..b8bad47327f22 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -3,6 +3,16 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "maps"], - "requiredPlugins": ["inspector"], + "requiredPlugins": [ + "inspector", + "licensing", + "home", + "data", + "fileUpload", + "uiActions", + "navigation", + "visualizations", + "embeddable" + ], "ui": true } diff --git a/x-pack/legacy/plugins/maps/public/_main.scss b/x-pack/plugins/maps/public/_main.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/_main.scss rename to x-pack/plugins/maps/public/_main.scss diff --git a/x-pack/legacy/plugins/maps/public/_mapbox_hacks.scss b/x-pack/plugins/maps/public/_mapbox_hacks.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/_mapbox_hacks.scss rename to x-pack/plugins/maps/public/_mapbox_hacks.scss diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index debead3ad5c45..c8db284a5c4f1 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -14,6 +14,7 @@ import { MapCenterAndZoom, MapRefreshConfig, } from '../../common/descriptor_types'; +import { MapSettings } from '../reducers/map'; export type SyncContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -62,3 +63,14 @@ export function hideViewControl(): AnyAction; export function setHiddenLayers(hiddenLayerIds: string[]): AnyAction; export function addLayerWithoutDataSync(layerDescriptor: unknown): AnyAction; + +export function setMapSettings(settings: MapSettings): AnyAction; + +export function rollbackMapSettings(): AnyAction; + +export function trackMapSettings(): AnyAction; + +export function updateMapSetting( + settingKey: string, + settingValue: string | boolean | number +): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/map_actions.js b/x-pack/plugins/maps/public/actions/map_actions.js index 13cb3d5f89860..da6ba6b481054 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.js +++ b/x-pack/plugins/maps/public/actions/map_actions.js @@ -4,6 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import turf from 'turf'; +import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; +import { + getLayerList, + getLayerListRaw, + getDataFilters, + getSelectedLayerId, + getMapReady, + getWaitingForMapReadyLayerListRaw, + getTransientLayerId, + getOpenTooltips, + getQuery, + getDataRequestDescriptor, +} from '../selectors/map_selectors'; + +import { FLYOUT_STATE } from '../reducers/ui'; +import { + cancelRequest, + registerCancelCallback, + unregisterCancelCallback, + getEventHandlers, +} from '../reducers/non_serializable_instances'; +import { updateFlyout } from './ui_actions'; +import { + FEATURE_ID_PROPERTY_NAME, + LAYER_TYPE, + SOURCE_DATA_ID_ORIGIN, +} from '../../common/constants'; + export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER'; export const SET_TRANSIENT_LAYER = 'SET_TRANSIENT_LAYER'; export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER'; @@ -45,3 +76,924 @@ export const HIDE_TOOLBAR_OVERLAY = 'HIDE_TOOLBAR_OVERLAY'; export const HIDE_LAYER_CONTROL = 'HIDE_LAYER_CONTROL'; export const HIDE_VIEW_CONTROL = 'HIDE_VIEW_CONTROL'; export const SET_WAITING_FOR_READY_HIDDEN_LAYERS = 'SET_WAITING_FOR_READY_HIDDEN_LAYERS'; +export const SET_MAP_SETTINGS = 'SET_MAP_SETTINGS'; +export const ROLLBACK_MAP_SETTINGS = 'ROLLBACK_MAP_SETTINGS'; +export const TRACK_MAP_SETTINGS = 'TRACK_MAP_SETTINGS'; +export const UPDATE_MAP_SETTING = 'UPDATE_MAP_SETTING'; + +function getLayerLoadingCallbacks(dispatch, getState, layerId) { + return { + startLoading: (dataId, requestToken, meta) => + dispatch(startDataLoad(layerId, dataId, requestToken, meta)), + stopLoading: (dataId, requestToken, data, meta) => + dispatch(endDataLoad(layerId, dataId, requestToken, data, meta)), + onLoadError: (dataId, requestToken, errorMessage) => + dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)), + updateSourceData: newData => { + dispatch(updateSourceDataRequest(layerId, newData)); + }, + isRequestStillActive: (dataId, requestToken) => { + const dataRequest = getDataRequestDescriptor(getState(), layerId, dataId); + if (!dataRequest) { + return false; + } + return dataRequest.dataRequestToken === requestToken; + }, + registerCancelCallback: (requestToken, callback) => + dispatch(registerCancelCallback(requestToken, callback)), + }; +} + +function getLayerById(layerId, state) { + return getLayerList(state).find(layer => { + return layerId === layer.getId(); + }); +} + +async function syncDataForAllLayers(dispatch, getState, dataFilters) { + const state = getState(); + const layerList = getLayerList(state); + const syncs = layerList.map(layer => { + const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layer.getId()); + return layer.syncData({ ...loadingFunctions, dataFilters }); + }); + await Promise.all(syncs); +} + +export function cancelAllInFlightRequests() { + return (dispatch, getState) => { + getLayerList(getState()).forEach(layer => { + dispatch(clearDataRequests(layer)); + }); + }; +} + +function clearDataRequests(layer) { + return dispatch => { + layer.getInFlightRequestTokens().forEach(requestToken => { + dispatch(cancelRequest(requestToken)); + }); + dispatch({ + type: UPDATE_LAYER_PROP, + id: layer.getId(), + propName: '__dataRequests', + newValue: [], + }); + }; +} + +export function setMapInitError(errorMessage) { + return { + type: SET_MAP_INIT_ERROR, + errorMessage, + }; +} + +export function setMapSettings(settings) { + return { + type: SET_MAP_SETTINGS, + settings, + }; +} + +export function rollbackMapSettings() { + return { type: ROLLBACK_MAP_SETTINGS }; +} + +export function trackMapSettings() { + return { type: TRACK_MAP_SETTINGS }; +} + +export function updateMapSetting(settingKey, settingValue) { + return { + type: UPDATE_MAP_SETTING, + settingKey, + settingValue, + }; +} + +export function trackCurrentLayerState(layerId) { + return { + type: TRACK_CURRENT_LAYER_STATE, + layerId: layerId, + }; +} + +export function rollbackToTrackedLayerStateForSelectedLayer() { + return async (dispatch, getState) => { + const layerId = getSelectedLayerId(getState()); + await dispatch({ + type: ROLLBACK_TO_TRACKED_LAYER_STATE, + layerId: layerId, + }); + + // Ensure updateStyleMeta is triggered + // syncDataForLayer may not trigger endDataLoad if no re-fetch is required + dispatch(updateStyleMeta(layerId)); + + dispatch(syncDataForLayer(layerId)); + }; +} + +export function removeTrackedLayerStateForSelectedLayer() { + return (dispatch, getState) => { + const layerId = getSelectedLayerId(getState()); + dispatch({ + type: REMOVE_TRACKED_LAYER_STATE, + layerId: layerId, + }); + }; +} + +export function replaceLayerList(newLayerList) { + return (dispatch, getState) => { + const isMapReady = getMapReady(getState()); + if (!isMapReady) { + dispatch({ + type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, + }); + } else { + getLayerListRaw(getState()).forEach(({ id }) => { + dispatch(removeLayerFromLayerList(id)); + }); + } + + newLayerList.forEach(layerDescriptor => { + dispatch(addLayer(layerDescriptor)); + }); + }; +} + +export function cloneLayer(layerId) { + return async (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer) { + return; + } + + const clonedDescriptor = await layer.cloneDescriptor(); + dispatch(addLayer(clonedDescriptor)); + }; +} + +export function addLayer(layerDescriptor) { + return (dispatch, getState) => { + const isMapReady = getMapReady(getState()); + if (!isMapReady) { + dispatch({ + type: ADD_WAITING_FOR_MAP_READY_LAYER, + layer: layerDescriptor, + }); + return; + } + + dispatch({ + type: ADD_LAYER, + layer: layerDescriptor, + }); + dispatch(syncDataForLayer(layerDescriptor.id)); + }; +} + +// Do not use when rendering a map. Method exists to enable selectors for getLayerList when +// rendering is not needed. +export function addLayerWithoutDataSync(layerDescriptor) { + return { + type: ADD_LAYER, + layer: layerDescriptor, + }; +} + +function setLayerDataLoadErrorStatus(layerId, errorMessage) { + return dispatch => { + dispatch({ + type: SET_LAYER_ERROR_STATUS, + isInErrorState: errorMessage !== null, + layerId, + errorMessage, + }); + }; +} + +export function cleanTooltipStateForLayer(layerId, layerFeatures = []) { + return (dispatch, getState) => { + let featuresRemoved = false; + const openTooltips = getOpenTooltips(getState()) + .map(tooltipState => { + const nextFeatures = tooltipState.features.filter(tooltipFeature => { + if (tooltipFeature.layerId !== layerId) { + // feature from another layer, keep it + return true; + } + + // Keep feature if it is still in layer + return layerFeatures.some(layerFeature => { + return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + }); + }); + + if (tooltipState.features.length !== nextFeatures.length) { + featuresRemoved = true; + } + + return { ...tooltipState, features: nextFeatures }; + }) + .filter(tooltipState => { + return tooltipState.features.length > 0; + }); + + if (featuresRemoved) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); + } + }; +} + +export function setLayerVisibility(layerId, makeVisible) { + return async (dispatch, getState) => { + //if the current-state is invisible, we also want to sync data + //e.g. if a layer was invisible at start-up, it won't have any data loaded + const layer = getLayerById(layerId, getState()); + + // If the layer visibility is already what we want it to be, do nothing + if (!layer || layer.isVisible() === makeVisible) { + return; + } + + if (!makeVisible) { + dispatch(cleanTooltipStateForLayer(layerId)); + } + + await dispatch({ + type: SET_LAYER_VISIBILITY, + layerId, + visibility: makeVisible, + }); + if (makeVisible) { + dispatch(syncDataForLayer(layerId)); + } + }; +} + +export function toggleLayerVisible(layerId) { + return async (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer) { + return; + } + const makeVisible = !layer.isVisible(); + + dispatch(setLayerVisibility(layerId, makeVisible)); + }; +} + +export function setSelectedLayer(layerId) { + return async (dispatch, getState) => { + const oldSelectedLayer = getSelectedLayerId(getState()); + if (oldSelectedLayer) { + await dispatch(rollbackToTrackedLayerStateForSelectedLayer()); + } + if (layerId) { + dispatch(trackCurrentLayerState(layerId)); + } + dispatch({ + type: SET_SELECTED_LAYER, + selectedLayerId: layerId, + }); + }; +} + +export function removeTransientLayer() { + return async (dispatch, getState) => { + const transientLayerId = getTransientLayerId(getState()); + if (transientLayerId) { + await dispatch(removeLayerFromLayerList(transientLayerId)); + await dispatch(setTransientLayer(null)); + } + }; +} + +export function setTransientLayer(layerId) { + return { + type: SET_TRANSIENT_LAYER, + transientLayerId: layerId, + }; +} + +export function clearTransientLayerStateAndCloseFlyout() { + return async dispatch => { + await dispatch(updateFlyout(FLYOUT_STATE.NONE)); + await dispatch(setSelectedLayer(null)); + await dispatch(removeTransientLayer()); + }; +} + +export function updateLayerOrder(newLayerOrder) { + return { + type: UPDATE_LAYER_ORDER, + newLayerOrder, + }; +} + +export function mapReady() { + return (dispatch, getState) => { + dispatch({ + type: MAP_READY, + }); + + getWaitingForMapReadyLayerListRaw(getState()).forEach(layerDescriptor => { + dispatch(addLayer(layerDescriptor)); + }); + + dispatch({ + type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, + }); + }; +} + +export function mapDestroyed() { + return { + type: MAP_DESTROYED, + }; +} + +export function mapExtentChanged(newMapConstants) { + return async (dispatch, getState) => { + const state = getState(); + const dataFilters = getDataFilters(state); + const { extent, zoom: newZoom } = newMapConstants; + const { buffer, zoom: currentZoom } = dataFilters; + + if (extent) { + let doesBufferContainExtent = false; + if (buffer) { + const bufferGeometry = turf.bboxPolygon([ + buffer.minLon, + buffer.minLat, + buffer.maxLon, + buffer.maxLat, + ]); + const extentGeometry = turf.bboxPolygon([ + extent.minLon, + extent.minLat, + extent.maxLon, + extent.maxLat, + ]); + + doesBufferContainExtent = turfBooleanContains(bufferGeometry, extentGeometry); + } + + if (!doesBufferContainExtent || currentZoom !== newZoom) { + const scaleFactor = 0.5; // TODO put scale factor in store and fetch with selector + const width = extent.maxLon - extent.minLon; + const height = extent.maxLat - extent.minLat; + dataFilters.buffer = { + minLon: extent.minLon - width * scaleFactor, + minLat: extent.minLat - height * scaleFactor, + maxLon: extent.maxLon + width * scaleFactor, + maxLat: extent.maxLat + height * scaleFactor, + }; + } + } + + dispatch({ + type: MAP_EXTENT_CHANGED, + mapState: { + ...dataFilters, + ...newMapConstants, + }, + }); + const newDataFilters = { ...dataFilters, ...newMapConstants }; + await syncDataForAllLayers(dispatch, getState, newDataFilters); + }; +} + +export function closeOnClickTooltip(tooltipId) { + return (dispatch, getState) => { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: getOpenTooltips(getState()).filter(({ id }) => { + return tooltipId !== id; + }), + }); + }; +} + +export function openOnClickTooltip(tooltipState) { + return (dispatch, getState) => { + const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => { + return ( + isLocked && + !_.isEqual(location, tooltipState.location) && + !_.isEqual(features, tooltipState.features) + ); + }); + + openTooltips.push({ + ...tooltipState, + isLocked: true, + id: uuid(), + }); + + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); + }; +} + +export function closeOnHoverTooltip() { + return (dispatch, getState) => { + if (getOpenTooltips(getState()).length) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: [], + }); + } + }; +} + +export function openOnHoverTooltip(tooltipState) { + return { + type: SET_OPEN_TOOLTIPS, + openTooltips: [ + { + ...tooltipState, + isLocked: false, + id: uuid(), + }, + ], + }; +} + +export function setMouseCoordinates({ lat, lon }) { + let safeLon = lon; + if (lon > 180) { + const overlapWestOfDateLine = lon - 180; + safeLon = -180 + overlapWestOfDateLine; + } else if (lon < -180) { + const overlapEastOfDateLine = Math.abs(lon) - 180; + safeLon = 180 - overlapEastOfDateLine; + } + + return { + type: SET_MOUSE_COORDINATES, + lat, + lon: safeLon, + }; +} + +export function clearMouseCoordinates() { + return { type: CLEAR_MOUSE_COORDINATES }; +} + +export function disableScrollZoom() { + return { type: SET_SCROLL_ZOOM, scrollZoom: false }; +} + +export function fitToLayerExtent(layerId) { + return async function(dispatch, getState) { + const targetLayer = getLayerById(layerId, getState()); + + if (targetLayer) { + const dataFilters = getDataFilters(getState()); + const bounds = await targetLayer.getBounds(dataFilters); + if (bounds) { + await dispatch(setGotoWithBounds(bounds)); + } + } + }; +} + +export function setGotoWithBounds(bounds) { + return { + type: SET_GOTO, + bounds: bounds, + }; +} + +export function setGotoWithCenter({ lat, lon, zoom }) { + return { + type: SET_GOTO, + center: { lat, lon, zoom }, + }; +} + +export function clearGoto() { + return { type: CLEAR_GOTO }; +} + +export function startDataLoad(layerId, dataId, requestToken, meta = {}) { + return (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (layer) { + dispatch(cancelRequest(layer.getPrevRequestToken(dataId))); + } + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoad) { + eventHandlers.onDataLoad({ + layerId, + dataId, + }); + } + + dispatch({ + meta, + type: LAYER_DATA_LOAD_STARTED, + layerId, + dataId, + requestToken, + }); + }; +} + +export function updateSourceDataRequest(layerId, newData) { + return dispatch => { + dispatch({ + type: UPDATE_SOURCE_DATA_REQUEST, + dataId: SOURCE_DATA_ID_ORIGIN, + layerId, + newData, + }); + + dispatch(updateStyleMeta(layerId)); + }; +} + +export function endDataLoad(layerId, dataId, requestToken, data, meta) { + return async (dispatch, getState) => { + dispatch(unregisterCancelCallback(requestToken)); + + const features = data && data.features ? data.features : []; + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadEnd) { + const layer = getLayerById(layerId, getState()); + const resultMeta = {}; + if (layer && layer.getType() === LAYER_TYPE.VECTOR) { + resultMeta.featuresCount = features.length; + } + + eventHandlers.onDataLoadEnd({ + layerId, + dataId, + resultMeta, + }); + } + + dispatch(cleanTooltipStateForLayer(layerId, features)); + dispatch({ + type: LAYER_DATA_LOAD_ENDED, + layerId, + dataId, + data, + meta, + requestToken, + }); + + //Clear any data-load errors when there is a succesful data return. + //Co this on end-data-load iso at start-data-load to avoid blipping the error status between true/false. + //This avoids jitter in the warning icon of the TOC when the requests continues to return errors. + dispatch(setLayerDataLoadErrorStatus(layerId, null)); + + dispatch(updateStyleMeta(layerId)); + }; +} + +export function onDataLoadError(layerId, dataId, requestToken, errorMessage) { + return async (dispatch, getState) => { + dispatch(unregisterCancelCallback(requestToken)); + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadError) { + eventHandlers.onDataLoadError({ + layerId, + dataId, + errorMessage, + }); + } + + dispatch(cleanTooltipStateForLayer(layerId)); + dispatch({ + type: LAYER_DATA_LOAD_ERROR, + data: null, + layerId, + dataId, + requestToken, + }); + + dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); + }; +} + +export function updateSourceProp(layerId, propName, value, newLayerType) { + return async dispatch => { + dispatch({ + type: UPDATE_SOURCE_PROP, + layerId, + propName, + value, + }); + if (newLayerType) { + dispatch(updateLayerType(layerId, newLayerType)); + } + await dispatch(clearMissingStyleProperties(layerId)); + dispatch(syncDataForLayer(layerId)); + }; +} + +function updateLayerType(layerId, newLayerType) { + return (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer || layer.getType() === newLayerType) { + return; + } + dispatch(clearDataRequests(layer)); + dispatch({ + type: UPDATE_LAYER_PROP, + id: layerId, + propName: 'type', + newValue: newLayerType, + }); + }; +} + +export function syncDataForLayer(layerId) { + return async (dispatch, getState) => { + const targetLayer = getLayerById(layerId, getState()); + if (targetLayer) { + const dataFilters = getDataFilters(getState()); + const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layerId); + await targetLayer.syncData({ + ...loadingFunctions, + dataFilters, + }); + } + }; +} + +export function updateLayerLabel(id, newLabel) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'label', + newValue: newLabel, + }; +} + +export function updateLayerMinZoom(id, minZoom) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'minZoom', + newValue: minZoom, + }; +} + +export function updateLayerMaxZoom(id, maxZoom) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'maxZoom', + newValue: maxZoom, + }; +} + +export function updateLayerAlpha(id, alpha) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'alpha', + newValue: alpha, + }; +} + +export function setLayerQuery(id, query) { + return dispatch => { + dispatch({ + type: UPDATE_LAYER_PROP, + id, + propName: 'query', + newValue: query, + }); + + dispatch(syncDataForLayer(id)); + }; +} + +export function removeSelectedLayer() { + return (dispatch, getState) => { + const state = getState(); + const layerId = getSelectedLayerId(state); + dispatch(removeLayer(layerId)); + }; +} + +export function removeLayer(layerId) { + return async (dispatch, getState) => { + const state = getState(); + const selectedLayerId = getSelectedLayerId(state); + if (layerId === selectedLayerId) { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + await dispatch(setSelectedLayer(null)); + } + dispatch(removeLayerFromLayerList(layerId)); + }; +} + +function removeLayerFromLayerList(layerId) { + return (dispatch, getState) => { + const layerGettingRemoved = getLayerById(layerId, getState()); + if (!layerGettingRemoved) { + return; + } + + layerGettingRemoved.getInFlightRequestTokens().forEach(requestToken => { + dispatch(cancelRequest(requestToken)); + }); + dispatch(cleanTooltipStateForLayer(layerId)); + layerGettingRemoved.destroy(); + dispatch({ + type: REMOVE_LAYER, + id: layerId, + }); + }; +} + +export function setQuery({ query, timeFilters, filters = [], refresh = false }) { + function generateQueryTimestamp() { + return new Date().toISOString(); + } + return async (dispatch, getState) => { + const prevQuery = getQuery(getState()); + const prevTriggeredAt = + prevQuery && prevQuery.queryLastTriggeredAt + ? prevQuery.queryLastTriggeredAt + : generateQueryTimestamp(); + + dispatch({ + type: SET_QUERY, + timeFilters, + query: { + ...query, + // ensure query changes to trigger re-fetch when "Refresh" clicked + queryLastTriggeredAt: refresh ? generateQueryTimestamp() : prevTriggeredAt, + }, + filters, + }); + + const dataFilters = getDataFilters(getState()); + await syncDataForAllLayers(dispatch, getState, dataFilters); + }; +} + +export function setRefreshConfig({ isPaused, interval }) { + return { + type: SET_REFRESH_CONFIG, + isPaused, + interval, + }; +} + +export function triggerRefreshTimer() { + return async (dispatch, getState) => { + dispatch({ + type: TRIGGER_REFRESH_TIMER, + }); + + const dataFilters = getDataFilters(getState()); + await syncDataForAllLayers(dispatch, getState, dataFilters); + }; +} + +export function clearMissingStyleProperties(layerId) { + return async (dispatch, getState) => { + const targetLayer = getLayerById(layerId, getState()); + if (!targetLayer) { + return; + } + + const style = targetLayer.getCurrentStyle(); + if (!style) { + return; + } + + const nextFields = await targetLayer.getFields(); //take into account all fields, since labels can be driven by any field (source or join) + const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved( + nextFields + ); + if (hasChanges) { + dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); + } + }; +} + +export function updateLayerStyle(layerId, styleDescriptor) { + return dispatch => { + dispatch({ + type: UPDATE_LAYER_STYLE, + layerId, + style: { + ...styleDescriptor, + }, + }); + + // Ensure updateStyleMeta is triggered + // syncDataForLayer may not trigger endDataLoad if no re-fetch is required + dispatch(updateStyleMeta(layerId)); + + // Style update may require re-fetch, for example ES search may need to retrieve field used for dynamic styling + dispatch(syncDataForLayer(layerId)); + }; +} + +export function updateStyleMeta(layerId) { + return async (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer) { + return; + } + const sourceDataRequest = layer.getSourceDataRequest(); + const style = layer.getCurrentStyle(); + if (!style || !sourceDataRequest) { + return; + } + const styleMeta = await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); + dispatch({ + type: SET_LAYER_STYLE_META, + layerId, + styleMeta, + }); + }; +} + +export function updateLayerStyleForSelectedLayer(styleDescriptor) { + return (dispatch, getState) => { + const selectedLayerId = getSelectedLayerId(getState()); + if (!selectedLayerId) { + return; + } + dispatch(updateLayerStyle(selectedLayerId, styleDescriptor)); + }; +} + +export function setJoinsForLayer(layer, joins) { + return async dispatch => { + await dispatch({ + type: SET_JOINS, + layer: layer, + joins: joins, + }); + + await dispatch(clearMissingStyleProperties(layer.getId())); + dispatch(syncDataForLayer(layer.getId())); + }; +} + +export function updateDrawState(drawState) { + return dispatch => { + if (drawState !== null) { + dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way + } + dispatch({ + type: UPDATE_DRAW_STATE, + drawState: drawState, + }); + }; +} + +export function disableInteractive() { + return { type: SET_INTERACTIVE, disableInteractive: true }; +} + +export function disableTooltipControl() { + return { type: DISABLE_TOOLTIP_CONTROL, disableTooltipControl: true }; +} + +export function hideToolbarOverlay() { + return { type: HIDE_TOOLBAR_OVERLAY, hideToolbarOverlay: true }; +} + +export function hideLayerControl() { + return { type: HIDE_LAYER_CONTROL, hideLayerControl: true }; +} +export function hideViewControl() { + return { type: HIDE_VIEW_CONTROL, hideViewControl: true }; +} + +export function setHiddenLayers(hiddenLayerIds) { + return (dispatch, getState) => { + const isMapReady = getMapReady(getState()); + + if (!isMapReady) { + dispatch({ type: SET_WAITING_FOR_READY_HIDDEN_LAYERS, hiddenLayerIds }); + } else { + getLayerListRaw(getState()).forEach(layer => + dispatch(setLayerVisibility(layer.id, !hiddenLayerIds.includes(layer.id))) + ); + } + }; +} diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js similarity index 98% rename from x-pack/legacy/plugins/maps/public/actions/map_actions.test.js rename to x-pack/plugins/maps/public/actions/map_actions.test.js index 7e2a3c827fa88..c280b8af7ab80 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -5,7 +5,7 @@ */ jest.mock('../selectors/map_selectors', () => ({})); -jest.mock('../../../../../plugins/maps/public/kibana_services', () => ({})); +jest.mock('../kibana_services', () => ({})); import { mapExtentChanged, setMouseCoordinates } from './map_actions'; diff --git a/x-pack/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/plugins/maps/public/actions/ui_actions.d.ts new file mode 100644 index 0000000000000..43cdcff7d2d69 --- /dev/null +++ b/x-pack/plugins/maps/public/actions/ui_actions.d.ts @@ -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 { AnyAction } from 'redux'; +import { FLYOUT_STATE } from '../reducers/ui'; + +export const UPDATE_FLYOUT: string; +export const CLOSE_SET_VIEW: string; +export const OPEN_SET_VIEW: string; +export const SET_IS_LAYER_TOC_OPEN: string; +export const SET_FULL_SCREEN: string; +export const SET_READ_ONLY: string; +export const SET_OPEN_TOC_DETAILS: string; +export const SHOW_TOC_DETAILS: string; +export const HIDE_TOC_DETAILS: string; +export const UPDATE_INDEXING_STAGE: string; + +export function updateFlyout(display: FLYOUT_STATE): AnyAction; + +export function setOpenTOCDetails(layerIds?: string[]): AnyAction; + +export function setIsLayerTOCOpen(open: boolean): AnyAction; + +export function setReadOnly(readOnly: boolean): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/ui_actions.js b/x-pack/plugins/maps/public/actions/ui_actions.js index 59ae56c15056a..e2a36e33e7db0 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.js +++ b/x-pack/plugins/maps/public/actions/ui_actions.js @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getFlyoutDisplay } from '../selectors/ui_selectors'; +import { FLYOUT_STATE } from '../reducers/ui'; +import { setSelectedLayer, trackMapSettings } from './map_actions'; + export const UPDATE_FLYOUT = 'UPDATE_FLYOUT'; export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW'; export const OPEN_SET_VIEW = 'OPEN_SET_VIEW'; @@ -14,3 +18,84 @@ export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; export const UPDATE_INDEXING_STAGE = 'UPDATE_INDEXING_STAGE'; + +export function exitFullScreen() { + return { + type: SET_FULL_SCREEN, + isFullScreen: false, + }; +} + +export function updateFlyout(display) { + return { + type: UPDATE_FLYOUT, + display, + }; +} +export function openMapSettings() { + return (dispatch, getState) => { + const flyoutDisplay = getFlyoutDisplay(getState()); + if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { + return; + } + dispatch(setSelectedLayer(null)); + dispatch(trackMapSettings()); + dispatch(updateFlyout(FLYOUT_STATE.MAP_SETTINGS_PANEL)); + }; +} +export function closeSetView() { + return { + type: CLOSE_SET_VIEW, + }; +} +export function openSetView() { + return { + type: OPEN_SET_VIEW, + }; +} +export function setIsLayerTOCOpen(isLayerTOCOpen) { + return { + type: SET_IS_LAYER_TOC_OPEN, + isLayerTOCOpen, + }; +} +export function enableFullScreen() { + return { + type: SET_FULL_SCREEN, + isFullScreen: true, + }; +} +export function setReadOnly(isReadOnly) { + return { + type: SET_READ_ONLY, + isReadOnly, + }; +} + +export function setOpenTOCDetails(layerIds) { + return { + type: SET_OPEN_TOC_DETAILS, + layerIds, + }; +} + +export function showTOCDetails(layerId) { + return { + type: SHOW_TOC_DETAILS, + layerId, + }; +} + +export function hideTOCDetails(layerId) { + return { + type: HIDE_TOC_DETAILS, + layerId, + }; +} + +export function updateIndexingStage(stage) { + return { + type: UPDATE_INDEXING_STAGE, + stage, + }; +} diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.d.ts b/x-pack/plugins/maps/public/angular/get_initial_layers.d.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_layers.d.ts rename to x-pack/plugins/maps/public/angular/get_initial_layers.d.ts diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.js b/x-pack/plugins/maps/public/angular/get_initial_layers.js new file mode 100644 index 0000000000000..1eb5dac309f28 --- /dev/null +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.js @@ -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 _ from 'lodash'; +// Import each layer type, even those not used, to init in registry + +import '../layers/sources/wms_source'; + +import '../layers/sources/ems_file_source'; + +import '../layers/sources/es_search_source'; + +import '../layers/sources/es_pew_pew_source/es_pew_pew_source'; + +import '../layers/sources/kibana_regionmap_source'; + +import '../layers/sources/es_geo_grid_source'; + +import '../layers/sources/xyz_tms_source'; + +import { KibanaTilemapSource } from '../layers/sources/kibana_tilemap_source'; + +import { EMSTMSSource } from '../layers/sources/ems_tms_source'; + +import { getInjectedVarFunc } from '../kibana_services'; + +import { getKibanaTileMap } from '../meta'; + +export function getInitialLayers(layerListJSON, initialLayers = []) { + if (layerListJSON) { + return JSON.parse(layerListJSON); + } + + const tilemapSourceFromKibana = getKibanaTileMap(); + if (_.get(tilemapSourceFromKibana, 'url')) { + const sourceDescriptor = KibanaTilemapSource.createDescriptor(); + const source = new KibanaTilemapSource(sourceDescriptor); + const layer = source.createDefaultLayer(); + return [layer.toLayerDescriptor(), ...initialLayers]; + } + + const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); + if (isEmsEnabled) { + const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); + const source = new EMSTMSSource(descriptor); + const layer = source.createDefaultLayer(); + return [layer.toLayerDescriptor(), ...initialLayers]; + } + + return initialLayers; +} diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js similarity index 79% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js rename to x-pack/plugins/maps/public/angular/get_initial_layers.test.js index 8c9185a16ea0e..4b5cad8d19260 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../../plugins/maps/public/meta', () => { +jest.mock('../meta', () => { return {}; }); -jest.mock('../../../../../plugins/maps/public/kibana_services'); +jest.mock('../kibana_services'); import { getInitialLayers } from './get_initial_layers'; @@ -15,8 +15,7 @@ const layerListNotProvided = undefined; describe('Saved object has layer list', () => { beforeEach(() => { - require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => - jest.fn(); + require('../kibana_services').getInjectedVarFunc = () => jest.fn(); }); it('Should get initial layers from saved object', () => { @@ -33,7 +32,7 @@ describe('Saved object has layer list', () => { describe('kibana.yml configured with map.tilemap.url', () => { beforeAll(() => { - require('../../../../../plugins/maps/public/meta').getKibanaTileMap = () => { + require('../meta').getKibanaTileMap = () => { return { url: 'myTileUrl', }; @@ -53,7 +52,7 @@ describe('kibana.yml configured with map.tilemap.url', () => { sourceDescriptor: { type: 'KIBANA_TILEMAP', }, - style: {}, + style: { type: 'TILE' }, type: 'TILE', visible: true, }, @@ -63,10 +62,10 @@ describe('kibana.yml configured with map.tilemap.url', () => { describe('EMS is enabled', () => { beforeAll(() => { - require('../../../../../plugins/maps/public/meta').getKibanaTileMap = () => { + require('../meta').getKibanaTileMap = () => { return null; }; - require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => key => { + require('../kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'emsTileLayerId': return { @@ -97,7 +96,7 @@ describe('EMS is enabled', () => { isAutoSelect: true, type: 'EMS_TMS', }, - style: {}, + style: { type: 'TILE' }, type: 'VECTOR_TILE', visible: true, }, @@ -107,11 +106,11 @@ describe('EMS is enabled', () => { describe('EMS is not enabled', () => { beforeAll(() => { - require('../../../../../plugins/maps/public/meta').getKibanaTileMap = () => { + require('../meta').getKibanaTileMap = () => { return null; }; - require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => key => { + require('../kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'isEmsEnabled': return false; diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js b/x-pack/plugins/maps/public/angular/get_initial_query.js similarity index 82% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_query.js rename to x-pack/plugins/maps/public/angular/get_initial_query.js index c50ecb2b05dc0..4f61142413671 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js +++ b/x-pack/plugins/maps/public/angular/get_initial_query.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; +import { getUiSettings } from '../kibana_services'; export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage }) { const settings = getUiSettings(); diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js similarity index 84% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js rename to x-pack/plugins/maps/public/angular/get_initial_refresh_config.js index 8735d45debfc4..f13e435cd1d5c 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js +++ b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; + +import { getUiSettings } from '../kibana_services'; export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { const uiSettings = getUiSettings(); diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js b/x-pack/plugins/maps/public/angular/get_initial_time_filters.js similarity index 80% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js rename to x-pack/plugins/maps/public/angular/get_initial_time_filters.js index 74fbf603e99f5..75d9f0e95ccf0 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js +++ b/x-pack/plugins/maps/public/angular/get_initial_time_filters.js @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; + +import { getUiSettings } from '../kibana_services'; export function getInitialTimeFilters({ mapStateJSON, globalState = {} }) { if (mapStateJSON) { diff --git a/x-pack/legacy/plugins/maps/public/angular/listing_ng_wrapper.html b/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html similarity index 100% rename from x-pack/legacy/plugins/maps/public/angular/listing_ng_wrapper.html rename to x-pack/plugins/maps/public/angular/listing_ng_wrapper.html diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/plugins/maps/public/angular/map.html similarity index 100% rename from x-pack/legacy/plugins/maps/public/angular/map.html rename to x-pack/plugins/maps/public/angular/map.html diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js similarity index 79% rename from x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js rename to x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index 710997a9c0d7f..2dcec35960b08 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -6,15 +6,14 @@ import _ from 'lodash'; import { createSavedGisMapClass } from './saved_gis_map'; -import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; +import { SavedObjectLoader } from '../../../../../../src/plugins/saved_objects/public'; import { getCoreChrome, getSavedObjectsClient, getIndexPatternService, getCoreOverlays, getData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/maps/public/kibana_services'; +} from '../../kibana_services'; export const getMapsSavedObjectLoader = _.once(function() { const services = { diff --git a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js similarity index 88% rename from x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js rename to x-pack/plugins/maps/public/angular/services/saved_gis_map.js index 990a0613da681..1a58b0cefaed9 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { createSavedObjectClass } from '../../../../../../../src/plugins/saved_objects/public'; +import { createSavedObjectClass } from '../../../../../../src/plugins/saved_objects/public'; import { getTimeFilters, getMapZoom, @@ -15,12 +15,13 @@ import { getRefreshConfig, getQuery, getFilters, + getMapSettings, } from '../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { convertMapExtentToPolygon } from '../../../../../../plugins/maps/public/elasticsearch_geo_utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../../../../../../plugins/maps/public/reducers/util'; + +import { convertMapExtentToPolygon } from '../../elasticsearch_geo_utils'; + +import { copyPersistentState } from '../../reducers/util'; import { extractReferences, injectReferences } from '../../../common/migrations/references'; import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; @@ -98,6 +99,7 @@ export function createSavedGisMapClass(services) { refreshConfig: getRefreshConfig(state), query: _.omit(getQuery(state), 'queryLastTriggeredAt'), filters: getFilters(state), + settings: getMapSettings(state), }); this.uiStateJSON = JSON.stringify({ diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap rename to x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap rename to x-pack/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/components/_geometry_filter.scss b/x-pack/plugins/maps/public/components/_geometry_filter.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/_geometry_filter.scss rename to x-pack/plugins/maps/public/components/_geometry_filter.scss diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss new file mode 100644 index 0000000000000..161b3fefdb8f9 --- /dev/null +++ b/x-pack/plugins/maps/public/components/_index.scss @@ -0,0 +1,3 @@ +@import 'metric_editors'; +@import './geometry_filter'; +@import 'tooltip_selector'; diff --git a/x-pack/legacy/plugins/maps/public/components/_metric_editors.scss b/x-pack/plugins/maps/public/components/_metric_editors.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/_metric_editors.scss rename to x-pack/plugins/maps/public/components/_metric_editors.scss diff --git a/x-pack/legacy/plugins/maps/public/components/_tooltip_selector.scss b/x-pack/plugins/maps/public/components/_tooltip_selector.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/_tooltip_selector.scss rename to x-pack/plugins/maps/public/components/_tooltip_selector.scss diff --git a/x-pack/plugins/maps/public/components/alpha_slider.tsx b/x-pack/plugins/maps/public/components/alpha_slider.tsx new file mode 100644 index 0000000000000..921c386292050 --- /dev/null +++ b/x-pack/plugins/maps/public/components/alpha_slider.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { ValidatedRange } from './validated_range'; + +interface Props { + alpha: number; + onChange: (alpha: number) => void; +} + +export function AlphaSlider({ alpha, onChange }: Props) { + const onAlphaChange = (newAlpha: number) => { + onChange(newAlpha / 100); + }; + + return ( + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/components/distance_filter_form.tsx b/x-pack/plugins/maps/public/components/distance_filter_form.tsx similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/distance_filter_form.tsx rename to x-pack/plugins/maps/public/components/distance_filter_form.tsx diff --git a/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/plugins/maps/public/components/geo_field_with_index.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts rename to x-pack/plugins/maps/public/components/geo_field_with_index.ts diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js b/x-pack/plugins/maps/public/components/geometry_filter_form.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js rename to x-pack/plugins/maps/public/components/geometry_filter_form.js diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.test.js b/x-pack/plugins/maps/public/components/geometry_filter_form.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/geometry_filter_form.test.js rename to x-pack/plugins/maps/public/components/geometry_filter_form.test.js diff --git a/x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js b/x-pack/plugins/maps/public/components/global_filter_checkbox.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js rename to x-pack/plugins/maps/public/components/global_filter_checkbox.js diff --git a/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.js b/x-pack/plugins/maps/public/components/layer_toc_actions.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/layer_toc_actions.js rename to x-pack/plugins/maps/public/components/layer_toc_actions.js diff --git a/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.test.js b/x-pack/plugins/maps/public/components/layer_toc_actions.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/layer_toc_actions.test.js rename to x-pack/plugins/maps/public/components/layer_toc_actions.test.js diff --git a/x-pack/legacy/plugins/maps/public/components/map_listing.js b/x-pack/plugins/maps/public/components/map_listing.js similarity index 98% rename from x-pack/legacy/plugins/maps/public/components/map_listing.js rename to x-pack/plugins/maps/public/components/map_listing.js index ef1d524cb91dd..ee10fe30130f3 100644 --- a/x-pack/legacy/plugins/maps/public/components/map_listing.js +++ b/x-pack/plugins/maps/public/components/map_listing.js @@ -7,8 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getToasts } from '../../../../../plugins/maps/public/kibana_services'; + +import { getToasts } from '../kibana_services'; import { EuiTitle, EuiFieldSearch, diff --git a/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx b/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx rename to x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx diff --git a/x-pack/plugins/maps/public/connected_components/_index.scss b/x-pack/plugins/maps/public/connected_components/_index.scss new file mode 100644 index 0000000000000..83042ae1d586c --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/_index.scss @@ -0,0 +1,6 @@ +@import 'gis_map/gis_map'; +@import 'layer_addpanel/source_select/index'; +@import 'layer_panel/index'; +@import 'widget_overlay/index'; +@import 'toolbar_overlay/index'; +@import 'map/features_tooltip/index'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/_gis_map.scss b/x-pack/plugins/maps/public/connected_components/gis_map/_gis_map.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/gis_map/_gis_map.scss rename to x-pack/plugins/maps/public/connected_components/gis_map/_gis_map.scss diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts b/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts new file mode 100644 index 0000000000000..92d92dfbd142d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts @@ -0,0 +1,15 @@ +/* + * 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 { Filter } from 'src/plugins/data/public'; + +import { RenderToolTipContent } from '../../layers/tooltips/tooltip_property'; + +export const GisMap: React.ComponentType<{ + addFilters: ((filters: Filter[]) => void) | null; + renderTooltipContent?: RenderToolTipContent; +}>; diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/plugins/maps/public/connected_components/gis_map/index.js new file mode 100644 index 0000000000000..f8769d0bb898a --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.js @@ -0,0 +1,46 @@ +/* + * 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 { connect } from 'react-redux'; +import { GisMap } from './view'; +import { exitFullScreen } from '../../actions/ui_actions'; +import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; +import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions'; +import { + areLayersLoaded, + getRefreshConfig, + getMapInitError, + getQueryableUniqueIndexPatternIds, + isToolbarOverlayHidden, +} from '../../selectors/map_selectors'; + +import { getCoreChrome } from '../../kibana_services'; + +function mapStateToProps(state = {}) { + return { + areLayersLoaded: areLayersLoaded(state), + flyoutDisplay: getFlyoutDisplay(state), + isFullScreen: getIsFullScreen(state), + refreshConfig: getRefreshConfig(state), + mapInitError: getMapInitError(state), + indexPatternIds: getQueryableUniqueIndexPatternIds(state), + hideToolbarOverlay: isToolbarOverlayHidden(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), + exitFullScreen: () => { + dispatch(exitFullScreen()); + getCoreChrome().setIsVisible(true); + }, + cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), + }; +} + +const connectedGisMap = connect(mapStateToProps, mapDispatchToProps)(GisMap); +export { connectedGisMap as GisMap }; diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js new file mode 100644 index 0000000000000..6eb173a001d01 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js @@ -0,0 +1,224 @@ +/* + * 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 _ from 'lodash'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { MBMapContainer } from '../map/mb'; +import { WidgetOverlay } from '../widget_overlay'; +import { ToolbarOverlay } from '../toolbar_overlay'; +import { LayerPanel } from '../layer_panel'; +import { AddLayerPanel } from '../layer_addpanel'; +import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; +import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; + +import { getIndexPatternsFromIds } from '../../index_pattern_util'; +import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; +import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public'; +import { i18n } from '@kbn/i18n'; +import uuid from 'uuid/v4'; +import { FLYOUT_STATE } from '../../reducers/ui'; +import { MapSettingsPanel } from '../map_settings_panel'; + +const RENDER_COMPLETE_EVENT = 'renderComplete'; + +export class GisMap extends Component { + state = { + isInitialLoadRenderTimeoutComplete: false, + domId: uuid(), + geoFields: [], + }; + + componentDidMount() { + this._isMounted = true; + this._isInitalLoadRenderTimerStarted = false; + this._setRefreshTimer(); + } + + componentDidUpdate() { + this._setRefreshTimer(); + if (this.props.areLayersLoaded && !this._isInitalLoadRenderTimerStarted) { + this._isInitalLoadRenderTimerStarted = true; + this._startInitialLoadRenderTimer(); + } + + if (!!this.props.addFilters) { + this._loadGeoFields(this.props.indexPatternIds); + } + } + + componentWillUnmount() { + this._isMounted = false; + this._clearRefreshTimer(); + this.props.cancelAllInFlightRequests(); + } + + // Reporting uses both a `data-render-complete` attribute and a DOM event listener to determine + // if a visualization is done loading. The process roughly is: + // - See if the `data-render-complete` attribute is "true". If so we're done! + // - If it's not, then reporting injects a listener into the browser for a custom "renderComplete" event. + // - When that event is fired, we snapshot the viz and move on. + // Failure to not have the dom attribute, or custom event, will timeout the job. + // See x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts for more. + _onInitialLoadRenderComplete = () => { + const el = document.querySelector(`[data-dom-id="${this.state.domId}"]`); + + if (el) { + el.dispatchEvent(new CustomEvent(RENDER_COMPLETE_EVENT, { bubbles: true })); + } + }; + + _loadGeoFields = async nextIndexPatternIds => { + if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { + // all ready loaded index pattern ids + return; + } + + this._prevIndexPatternIds = nextIndexPatternIds; + + const geoFields = []; + try { + const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); + indexPatterns.forEach(indexPattern => { + indexPattern.fields.forEach(field => { + if ( + !indexPatternsUtils.isNestedField(field) && + (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || + field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) + ) { + geoFields.push({ + geoFieldName: field.name, + geoFieldType: field.type, + indexPatternTitle: indexPattern.title, + indexPatternId: indexPattern.id, + }); + } + }); + }); + } catch (e) { + // swallow errors. + // the Layer-TOC will indicate which layers are disfunctional on a per-layer basis + } + + if (!this._isMounted) { + return; + } + + this.setState({ geoFields }); + }; + + _setRefreshTimer = () => { + const { isPaused, interval } = this.props.refreshConfig; + + if (this.isPaused === isPaused && this.interval === interval) { + // refreshConfig is the same, nothing to do + return; + } + + this.isPaused = isPaused; + this.interval = interval; + + this._clearRefreshTimer(); + + if (!isPaused && interval > 0) { + this.refreshTimerId = setInterval(() => { + this.props.triggerRefreshTimer(); + }, interval); + } + }; + + _clearRefreshTimer = () => { + if (this.refreshTimerId) { + clearInterval(this.refreshTimerId); + } + }; + + // Mapbox does not provide any feedback when rendering is complete. + // Temporary solution is just to wait set period of time after data has loaded. + _startInitialLoadRenderTimer = () => { + setTimeout(() => { + if (this._isMounted) { + this.setState({ isInitialLoadRenderTimeoutComplete: true }); + this._onInitialLoadRenderComplete(); + } + }, 5000); + }; + + render() { + const { + addFilters, + flyoutDisplay, + isFullScreen, + exitFullScreen, + mapInitError, + renderTooltipContent, + } = this.props; + + const { domId } = this.state; + + if (mapInitError) { + return ( +
+ +

{mapInitError}

+
+
+ ); + } + + let flyoutPanel = null; + if (flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD) { + flyoutPanel = ; + } else if (flyoutDisplay === FLYOUT_STATE.LAYER_PANEL) { + flyoutPanel = ; + } else if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { + flyoutPanel = ; + } + + let exitFullScreenButton; + if (isFullScreen) { + exitFullScreenButton = ; + } + return ( + + + + {!this.props.hideToolbarOverlay && ( + + )} + + + + + {flyoutPanel} + + + {exitFullScreenButton} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js new file mode 100644 index 0000000000000..bff235a7d27fc --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { ImportEditor } from './view'; + +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; + +import { INDEXING_STAGE } from '../../../reducers/ui'; +import { updateIndexingStage } from '../../../actions/ui_actions'; +import { getIndexingStage } from '../../../selectors/ui_selectors'; + +function mapStateToProps(state = {}) { + return { + inspectorAdapters: getInspectorAdapters(state), + isIndexingTriggered: getIndexingStage(state) === INDEXING_STAGE.TRIGGERED, + }; +} + +const mapDispatchToProps = { + onIndexReady: indexReady => + indexReady ? updateIndexingStage(INDEXING_STAGE.READY) : updateIndexingStage(null), + importSuccessHandler: () => updateIndexingStage(INDEXING_STAGE.SUCCESS), + importErrorHandler: () => updateIndexingStage(INDEXING_STAGE.ERROR), +}; + +const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(ImportEditor); +export { connectedFlyOut as ImportEditor }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js new file mode 100644 index 0000000000000..a4fa0d492bf3f --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js @@ -0,0 +1,54 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiSpacer, EuiPanel, EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { uploadLayerWizardConfig } from '../../../layers/sources/client_file_source'; + +export const ImportEditor = ({ clearSource, isIndexingTriggered, ...props }) => { + const editorProperties = getEditorProperties({ isIndexingTriggered, ...props }); + return ( + + {isIndexingTriggered ? null : ( + + + + + + + )} + + {uploadLayerWizardConfig.renderWizard(editorProperties)} + + + ); +}; + +function getEditorProperties({ + inspectorAdapters, + onRemove, + viewLayer, + isIndexingTriggered, + onIndexReady, + importSuccessHandler, + importErrorHandler, +}) { + return { + onPreviewSource: viewLayer, + inspectorAdapters, + onRemove, + importSuccessHandler, + importErrorHandler, + isIndexingTriggered, + addAndViewSource: viewLayer, + onIndexReady, + }; +} diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js new file mode 100644 index 0000000000000..24c1f5ced4fe6 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { AddLayerPanel } from './view'; + +import { FLYOUT_STATE, INDEXING_STAGE } from '../../reducers/ui'; +import { updateFlyout, updateIndexingStage } from '../../actions/ui_actions'; +import { getFlyoutDisplay, getIndexingStage } from '../../selectors/ui_selectors'; +import { getMapColors } from '../../selectors/map_selectors'; + +import { getInspectorAdapters } from '../../reducers/non_serializable_instances'; +import { + setTransientLayer, + addLayer, + setSelectedLayer, + removeTransientLayer, +} from '../../actions/map_actions'; + +function mapStateToProps(state = {}) { + const indexingStage = getIndexingStage(state); + return { + inspectorAdapters: getInspectorAdapters(state), + flyoutVisible: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + mapColors: getMapColors(state), + isIndexingTriggered: indexingStage === INDEXING_STAGE.TRIGGERED, + isIndexingSuccess: indexingStage === INDEXING_STAGE.SUCCESS, + isIndexingReady: indexingStage === INDEXING_STAGE.READY, + }; +} + +function mapDispatchToProps(dispatch) { + return { + viewLayer: async layer => { + await dispatch(setSelectedLayer(null)); + await dispatch(removeTransientLayer()); + dispatch(addLayer(layer.toLayerDescriptor())); + dispatch(setSelectedLayer(layer.getId())); + dispatch(setTransientLayer(layer.getId())); + }, + removeTransientLayer: () => { + dispatch(setSelectedLayer(null)); + dispatch(removeTransientLayer()); + }, + selectLayerAndAdd: () => { + dispatch(setTransientLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); + }, + setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)), + resetIndexing: () => dispatch(updateIndexingStage(null)), + }; +} + +const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })( + AddLayerPanel +); +export { connectedFlyOut as AddLayerPanel }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js new file mode 100644 index 0000000000000..8937f32d3bf05 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js @@ -0,0 +1,19 @@ +/* + * 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 { connect } from 'react-redux'; +import { SourceEditor } from './view'; + +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; + +function mapStateToProps(state = {}) { + return { + inspectorAdapters: getInspectorAdapters(state), + }; +} + +const connectedFlyOut = connect(mapStateToProps)(SourceEditor); +export { connectedFlyOut as SourceEditor }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss new file mode 100644 index 0000000000000..8ae6970315e13 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss @@ -0,0 +1 @@ +@import 'source_select'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js similarity index 90% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js index 67cc17ebaa224..80b05a0fd015b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js @@ -5,8 +5,8 @@ */ import React, { Fragment } from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getLayerWizards } from '../../../../../../../plugins/maps/public/layers/layer_wizard_registry'; + +import { getLayerWizards } from '../../../layers/layer_wizard_registry'; import { EuiTitle, EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss new file mode 100644 index 0000000000000..41b4826a02c67 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss @@ -0,0 +1,4 @@ +@import 'layer_panel'; +@import 'filter_editor/filter_editor'; +@import 'join_editor/resources/join'; +@import 'style_settings/style_settings'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js similarity index 96% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 40fdac38493d4..fba2ec05d0b1d 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -20,12 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - getIndexPatternService, - getUiSettings, - getData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/maps/public/kibana_services'; +import { getIndexPatternService, getUiSettings, getData } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; export class FilterEditor extends Component { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js new file mode 100644 index 0000000000000..621ce209eb982 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { FlyoutFooter } from './view'; + +import { FLYOUT_STATE } from '../../../reducers/ui'; +import { updateFlyout } from '../../../actions/ui_actions'; +import { hasDirtyState } from '../../../selectors/map_selectors'; +import { + setSelectedLayer, + removeSelectedLayer, + removeTrackedLayerStateForSelectedLayer, +} from '../../../actions/map_actions'; + +function mapStateToProps(state = {}) { + return { + hasStateChanged: hasDirtyState(state), + }; +} + +const mapDispatchToProps = dispatch => { + return { + cancelLayerPanel: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(setSelectedLayer(null)); + }, + saveLayerEdits: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removeTrackedLayerStateForSelectedLayer()); + dispatch(setSelectedLayer(null)); + }, + removeLayer: () => { + dispatch(removeSelectedLayer()); + }, + }; +}; + +const connectedFlyoutFooter = connect(mapStateToProps, mapDispatchToProps)(FlyoutFooter); +export { connectedFlyoutFooter as FlyoutFooter }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js similarity index 95% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js index 9c4e1cfdb5467..0d26354e2449b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js @@ -13,9 +13,9 @@ import { MetricsExpression } from './metrics_expression'; import { WhereExpression } from './where_expression'; import { GlobalFilterCheckbox } from '../../../../components/global_filter_checkbox'; -import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getIndexPatternService } from '../../../../../../../../plugins/maps/public/kibana_services'; +import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; + +import { getIndexPatternService } from '../../../../kibana_services'; export class Join extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js similarity index 93% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 73600c81d221e..12ca2f3c514a0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -16,16 +16,15 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SingleFieldSelect } from '../../../../../../../../plugins/maps/public/components/single_field_select'; + +import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getTermsFields } from '../../../../../../../../plugins/maps/public/index_pattern_util'; + +import { getTermsFields } from '../../../../index_pattern_util'; import { getIndexPatternService, getIndexPatternSelectComponent, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/maps/public/kibana_services'; +} from '../../../../kibana_services'; export class JoinExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js similarity index 95% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js index c6a79a398f9af..8c83743ac4c96 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js @@ -14,8 +14,8 @@ import { EuiFormErrorText, EuiFormHelpText, } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsEditor } from '../../../../../../../../plugins/maps/public/components/metrics_editor'; + +import { MetricsEditor } from '../../../../components/metrics_editor'; import { FormattedMessage } from '@kbn/i18n/react'; import { AGG_TYPE } from '../../../../../common/constants'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js similarity index 92% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js index d8bf862249448..3cd8a3c42879a 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../../../../../plugins/maps/public/components/metric_editor', () => ({ +jest.mock('../../../../components/metric_editor', () => ({ MetricsEditor: () => { return
mockMetricsEditor
; }, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js similarity index 94% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index 54ec0ac46fa3d..7c9b4f7b7b9a4 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -8,11 +8,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiExpression, EuiFormHelpText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - getUiSettings, - getData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/maps/public/kibana_services'; +import { getUiSettings, getData } from '../../../../kibana_services'; export class WhereExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js new file mode 100644 index 0000000000000..e2f22c584d3b3 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js @@ -0,0 +1,41 @@ +/* + * 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 { connect } from 'react-redux'; +import { LayerSettings } from './layer_settings'; +import { getSelectedLayer } from '../../../selectors/map_selectors'; +import { + updateLayerLabel, + updateLayerMaxZoom, + updateLayerMinZoom, + updateLayerAlpha, +} from '../../../actions/map_actions'; +import { MAX_ZOOM } from '../../../../common/constants'; + +function mapStateToProps(state = {}) { + const selectedLayer = getSelectedLayer(state); + return { + minVisibilityZoom: selectedLayer.getMinSourceZoom(), + maxVisibilityZoom: MAX_ZOOM, + alpha: selectedLayer.getAlpha(), + label: selectedLayer.getLabel(), + layerId: selectedLayer.getId(), + maxZoom: selectedLayer.getMaxZoom(), + minZoom: selectedLayer.getMinZoom(), + }; +} + +function mapDispatchToProps(dispatch) { + return { + updateLabel: (id, label) => dispatch(updateLayerLabel(id, label)), + updateMinZoom: (id, minZoom) => dispatch(updateLayerMinZoom(id, minZoom)), + updateMaxZoom: (id, maxZoom) => dispatch(updateLayerMaxZoom(id, maxZoom)), + updateAlpha: (id, alpha) => dispatch(updateLayerAlpha(id, alpha)), + }; +} + +const connectedLayerSettings = connect(mapStateToProps, mapDispatchToProps)(LayerSettings); +export { connectedLayerSettings as LayerSettings }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js new file mode 100644 index 0000000000000..d84d05260f982 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -0,0 +1,87 @@ +/* + * 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, { Fragment } from 'react'; + +import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; + +import { AlphaSlider } from '../../../components/alpha_slider'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; +export function LayerSettings(props) { + const onLabelChange = event => { + const label = event.target.value; + props.updateLabel(props.layerId, label); + }; + + const onZoomChange = ([min, max]) => { + props.updateMinZoom(props.layerId, Math.max(props.minVisibilityZoom, parseInt(min, 10))); + props.updateMaxZoom(props.layerId, Math.min(props.maxVisibilityZoom, parseInt(max, 10))); + }; + + const onAlphaChange = alpha => { + props.updateAlpha(props.layerId, alpha); + }; + + const renderZoomSliders = () => { + return ( + + ); + }; + + const renderLabel = () => { + return ( + + + + ); + }; + + return ( + + + +
+ +
+
+ + + {renderLabel()} + {renderZoomSliders()} + +
+ + +
+ ); +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js new file mode 100644 index 0000000000000..f8b7c417e67fd --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js @@ -0,0 +1,238 @@ +/* + * 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, { Fragment } from 'react'; + +import { FilterEditor } from './filter_editor'; +import { JoinEditor } from './join_editor'; +import { FlyoutFooter } from './flyout_footer'; +import { LayerErrors } from './layer_errors'; +import { LayerSettings } from './layer_settings'; +import { StyleSettings } from './style_settings'; +import { + EuiButtonIcon, + EuiFlexItem, + EuiTitle, + EuiPanel, + EuiFlexGroup, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiSpacer, + EuiAccordion, + EuiText, + EuiLink, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +import { getData, getCore } from '../../kibana_services'; + +const localStorage = new Storage(window.localStorage); + +export class LayerPanel extends React.Component { + state = { + displayName: '', + immutableSourceProps: [], + leftJoinFields: null, + }; + + componentDidMount() { + this._isMounted = true; + this.loadDisplayName(); + this.loadImmutableSourceProperties(); + this.loadLeftJoinFields(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + loadDisplayName = async () => { + if (!this.props.selectedLayer) { + return; + } + + const displayName = await this.props.selectedLayer.getDisplayName(); + if (this._isMounted) { + this.setState({ displayName }); + } + }; + + loadImmutableSourceProperties = async () => { + if (!this.props.selectedLayer) { + return; + } + + const immutableSourceProps = await this.props.selectedLayer.getImmutableSourceProperties(); + if (this._isMounted) { + this.setState({ immutableSourceProps }); + } + }; + + async loadLeftJoinFields() { + if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) { + return; + } + + let leftJoinFields; + try { + const leftFieldsInstances = await this.props.selectedLayer.getLeftJoinFields(); + const leftFieldPromises = leftFieldsInstances.map(async field => { + return { + name: field.getName(), + label: await field.getLabel(), + }; + }); + leftJoinFields = await Promise.all(leftFieldPromises); + } catch (error) { + leftJoinFields = []; + } + if (this._isMounted) { + this.setState({ leftJoinFields }); + } + } + + _onSourceChange = ({ propName, value, newLayerType }) => { + this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); + }; + + _renderFilterSection() { + if (!this.props.selectedLayer.supportsElasticsearchFilters()) { + return null; + } + + return ( + + + + + + + ); + } + + _renderJoinSection() { + if (!this.props.selectedLayer.isJoinable()) { + return null; + } + + return ( + + + + + + + ); + } + + _renderSourceProperties() { + return this.state.immutableSourceProps.map(({ label, value, link }) => { + function renderValue() { + if (link) { + return ( + + {value} + + ); + } + return {value}; + } + return ( +

+ {label} {renderValue()} +

+ ); + }); + } + + render() { + const { selectedLayer } = this.props; + + if (!selectedLayer) { + return null; + } + + return ( + + + + + + + + + + + +

{this.state.displayName}

+
+
+
+ +
+ + + + {this._renderSourceProperties()} + + +
+
+ +
+
+ + + + + {this.props.selectedLayer.renderSourceSettingsEditor({ + onChange: this._onSourceChange, + })} + + {this._renderFilterSection()} + + {this._renderJoinSection()} + + +
+
+ + + + +
+
+ ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/_index.scss b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/_index.scss rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js similarity index 92% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index 15824b82965e8..b103fb43af97c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -8,12 +8,12 @@ import React, { Component, Fragment } from 'react'; import { EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createSpatialFilterWithGeometry } from '../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; + +import { createSpatialFilterWithGeometry } from '../../../elasticsearch_geo_utils'; import { GEO_JSON_TYPE } from '../../../../common/constants'; import { GeometryFilterForm } from '../../../components/geometry_filter_form'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { UrlOverflowService } from '../../../../../../../../src/plugins/kibana_legacy/public'; + +import { UrlOverflowService } from '../../../../../../../src/plugins/kibana_legacy/public'; import rison from 'rison-node'; // over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped. diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js similarity index 86% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index cc0e665525036..a69e06458a6a0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -15,8 +15,7 @@ import { createSpatialFilterWithGeometry, getBoundingBoxGeometry, roundCoordinates, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; +} from '../../../../elasticsearch_geo_utils'; import { DrawTooltip } from './draw_tooltip'; const mbDrawModes = MapboxDraw.modes; @@ -65,13 +64,28 @@ export class DrawControl extends React.Component { if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { const circle = e.features[0]; - roundCoordinates(circle.properties.center); + const distanceKm = _.round( + circle.properties.radiusKm, + circle.properties.radiusKm > 10 ? 0 : 2 + ); + // Only include as much precision as needed for distance + let precision = 2; + if (distanceKm <= 1) { + precision = 5; + } else if (distanceKm <= 10) { + precision = 4; + } else if (distanceKm <= 100) { + precision = 3; + } const filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel, - distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2), + distanceKm, geoFieldName: this.props.drawState.geoFieldName, indexPatternId: this.props.drawState.indexPatternId, - point: circle.properties.center, + point: [ + _.round(circle.properties.center[0], precision), + _.round(circle.properties.center[1], precision), + ], }); this.props.addFilters([filter]); this.props.disableDrawState(); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/index.js rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/index.js diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/index.js new file mode 100644 index 0000000000000..f8daf0804265b --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map/mb/index.js @@ -0,0 +1,79 @@ +/* + * 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 { connect } from 'react-redux'; +import { MBMapContainer } from './view'; +import { + mapExtentChanged, + mapReady, + mapDestroyed, + setMouseCoordinates, + clearMouseCoordinates, + clearGoto, + setMapInitError, +} from '../../../actions/map_actions'; +import { + getLayerList, + getMapReady, + getGoto, + getScrollZoom, + isInteractiveDisabled, + isTooltipControlDisabled, + isViewControlHidden, + getSpatialFiltersLayer, + getMapSettings, +} from '../../../selectors/map_selectors'; + +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; + +function mapStateToProps(state = {}) { + return { + isMapReady: getMapReady(state), + settings: getMapSettings(state), + layerList: getLayerList(state), + spatialFiltersLayer: getSpatialFiltersLayer(state), + goto: getGoto(state), + inspectorAdapters: getInspectorAdapters(state), + scrollZoom: getScrollZoom(state), + disableInteractive: isInteractiveDisabled(state), + disableTooltipControl: isTooltipControlDisabled(state), + disableTooltipControl: isTooltipControlDisabled(state), + hideViewControl: isViewControlHidden(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + extentChanged: e => { + dispatch(mapExtentChanged(e)); + }, + onMapReady: e => { + dispatch(clearGoto()); + dispatch(mapExtentChanged(e)); + dispatch(mapReady()); + }, + onMapDestroyed: () => { + dispatch(mapDestroyed()); + }, + setMouseCoordinates: ({ lat, lon }) => { + dispatch(setMouseCoordinates({ lat, lon })); + }, + clearMouseCoordinates: () => { + dispatch(clearMouseCoordinates()); + }, + clearGoto: () => { + dispatch(clearGoto()); + }, + setMapInitError(errorMessage) { + dispatch(setMapInitError(errorMessage)); + }, + }; +} + +const connectedMBMapContainer = connect(mapStateToProps, mapDispatchToProps, null, { + forwardRef: true, +})(MBMapContainer); +export { connectedMBMapContainer as MBMapContainer }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js similarity index 82% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js rename to x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js index a8c4f61a00da3..4774cdc556c24 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js @@ -5,6 +5,7 @@ */ import { removeOrphanedSourcesAndLayers, syncLayerOrderForSingleLayer } from './utils'; +import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; import _ from 'lodash'; class MockMbMap { @@ -121,7 +122,8 @@ function makeMultiSourceMockLayer(layerId) { ); } -describe('mb/utils', () => { +describe('removeOrphanedSourcesAndLayers', () => { + const spatialFilterLayer = makeMultiSourceMockLayer(SPATIAL_FILTERS_LAYER_ID); test('should remove foo and bar layer', async () => { const bazLayer = makeSingleSourceMockLayer('baz'); const fooLayer = makeSingleSourceMockLayer('foo'); @@ -133,7 +135,7 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); @@ -151,7 +153,7 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); @@ -169,13 +171,23 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); expect(removedStyle).toEqual(nextStyle); }); + test('should not remove spatial filter layer and sources when spatialFilterLayer is provided', async () => { + const styleWithSpatialFilters = getMockStyle([spatialFilterLayer]); + const mockMbMap = new MockMbMap(styleWithSpatialFilters); + + removeOrphanedSourcesAndLayers(mockMbMap, [], spatialFilterLayer); + expect(mockMbMap.getStyle()).toEqual(styleWithSpatialFilters); + }); +}); + +describe('syncLayerOrderForSingleLayer', () => { test('should move bar layer in front of foo layer', async () => { const fooLayer = makeSingleSourceMockLayer('foo'); const barLayer = makeSingleSourceMockLayer('bar'); @@ -250,40 +262,4 @@ describe('mb/utils', () => { const nextStyle = getMockStyle(nextLayerListOrder); expect(orderedStyle).toEqual(nextStyle); }); - - test('should reorder foo and bar and remove baz', async () => { - const bazLayer = makeSingleSourceMockLayer('baz'); - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeSingleSourceMockLayer('bar'); - - const currentLayerOrder = [bazLayer, fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); - - test('should reorder foo and bar and remove baz, when having multi-source multi-layer data', async () => { - const bazLayer = makeMultiSourceMockLayer('baz'); - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeMultiSourceMockLayer('bar'); - - const currentLayerOrder = [bazLayer, fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js index 15aacfbf1f38d..adf109a087d27 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js @@ -4,8 +4,130 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import { RGBAImage } from './image_utils'; +export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLayer) { + const mbStyle = mbMap.getStyle(); + + const mbLayerIdsToRemove = []; + mbStyle.layers.forEach(mbLayer => { + // ignore mapbox layers from spatial filter layer + if (spatialFilterLayer.ownsMbLayerId(mbLayer.id)) { + return; + } + + const layer = layerList.find(layer => { + return layer.ownsMbLayerId(mbLayer.id); + }); + if (!layer) { + mbLayerIdsToRemove.push(mbLayer.id); + } + }); + mbLayerIdsToRemove.forEach(mbLayerId => mbMap.removeLayer(mbLayerId)); + + const mbSourcesToRemove = []; + for (const mbSourceId in mbStyle.sources) { + if (mbStyle.sources.hasOwnProperty(mbSourceId)) { + // ignore mapbox sources from spatial filter layer + if (spatialFilterLayer.ownsMbSourceId(mbSourceId)) { + return; + } + + const layer = layerList.find(layer => { + return layer.ownsMbSourceId(mbSourceId); + }); + if (!layer) { + mbSourcesToRemove.push(mbSourceId); + } + } + } + mbSourcesToRemove.forEach(mbSourceId => mbMap.removeSource(mbSourceId)); +} + +export function moveLayerToTop(mbMap, layer) { + const mbStyle = mbMap.getStyle(); + + if (!mbStyle.layers || mbStyle.layers.length === 0) { + return; + } + + layer.getMbLayerIds().forEach(mbLayerId => { + const mbLayer = mbMap.getLayer(mbLayerId); + if (mbLayer) { + mbMap.moveLayer(mbLayerId); + } + }); +} + +/** + * This is function assumes only a single layer moved in the layerList, compared to mbMap + * It is optimized to minimize the amount of mbMap.moveLayer calls. + * @param mbMap + * @param layerList + */ +export function syncLayerOrderForSingleLayer(mbMap, layerList) { + if (!layerList || layerList.length === 0) { + return; + } + + const mbLayers = mbMap.getStyle().layers.slice(); + const layerIds = []; + mbLayers.forEach(mbLayer => { + const layer = layerList.find(layer => layer.ownsMbLayerId(mbLayer.id)); + if (layer) { + layerIds.push(layer.getId()); + } + }); + + const currentLayerOrderLayerIds = _.uniq(layerIds); + + const newLayerOrderLayerIdsUnfiltered = layerList.map(l => l.getId()); + const newLayerOrderLayerIds = newLayerOrderLayerIdsUnfiltered.filter(layerId => + currentLayerOrderLayerIds.includes(layerId) + ); + + let netPos = 0; + let netNeg = 0; + const movementArr = currentLayerOrderLayerIds.reduce((accu, id, idx) => { + const movement = newLayerOrderLayerIds.findIndex(newOId => newOId === id) - idx; + movement > 0 ? netPos++ : movement < 0 && netNeg++; + accu.push({ id, movement }); + return accu; + }, []); + if (netPos === 0 && netNeg === 0) { + return; + } + const movedLayerId = + (netPos >= netNeg && movementArr.find(l => l.movement < 0).id) || + (netPos < netNeg && movementArr.find(l => l.movement > 0).id); + const nextLayerIdx = newLayerOrderLayerIds.findIndex(layerId => layerId === movedLayerId) + 1; + + let nextMbLayerId; + if (nextLayerIdx === newLayerOrderLayerIds.length) { + nextMbLayerId = null; + } else { + const foundLayer = mbLayers.find(({ id: mbLayerId }) => { + const layerId = newLayerOrderLayerIds[nextLayerIdx]; + const layer = layerList.find(layer => layer.getId() === layerId); + return layer.ownsMbLayerId(mbLayerId); + }); + nextMbLayerId = foundLayer.id; + } + + const movedLayer = layerList.find(layer => layer.getId() === movedLayerId); + mbLayers.forEach(({ id: mbLayerId }) => { + if (movedLayer.ownsMbLayerId(mbLayerId)) { + mbMap.moveLayer(mbLayerId, nextMbLayerId); + } + }); +} + +export async function addSpritesheetToMap(json, imgUrl, mbMap) { + const imgData = await loadSpriteSheetImageData(imgUrl); + addSpriteSheetToMapFromImageData(json, imgData, mbMap); +} + function getImageData(img) { const canvas = window.document.createElement('canvas'); const context = canvas.getContext('2d'); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js new file mode 100644 index 0000000000000..6bb5a4fed6e52 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -0,0 +1,337 @@ +/* + * 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 _ from 'lodash'; +import React from 'react'; +import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; +import { + syncLayerOrderForSingleLayer, + removeOrphanedSourcesAndLayers, + addSpritesheetToMap, + moveLayerToTop, +} from './utils'; +import { getGlyphUrl, isRetina } from '../../../meta'; +import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; +import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; +import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; +import { spritesheet } from '@elastic/maki'; +import sprites1 from '@elastic/maki/dist/sprite@1.png'; +import sprites2 from '@elastic/maki/dist/sprite@2.png'; +import { DrawControl } from './draw_control'; +import { TooltipControl } from './tooltip_control'; +import { clampToLatBounds, clampToLonBounds } from '../../../elasticsearch_geo_utils'; + +import { getInjectedVarFunc } from '../../../kibana_services'; + +mapboxgl.workerUrl = mbWorkerUrl; +mapboxgl.setRTLTextPlugin(mbRtlPlugin); + +export class MBMapContainer extends React.Component { + state = { + prevLayerList: undefined, + hasSyncedLayerList: false, + mbMap: undefined, + }; + + static getDerivedStateFromProps(nextProps, prevState) { + const nextLayerList = nextProps.layerList; + if (nextLayerList !== prevState.prevLayerList) { + return { + prevLayerList: nextLayerList, + hasSyncedLayerList: false, + }; + } + + return null; + } + + componentDidMount() { + this._initializeMap(); + this._isMounted = true; + } + + componentDidUpdate() { + if (this.state.mbMap) { + // do not debounce syncing of map-state + this._syncMbMapWithMapState(); + this._debouncedSync(); + } + } + + componentWillUnmount() { + this._isMounted = false; + if (this._checker) { + this._checker.destroy(); + } + if (this.state.mbMap) { + this.state.mbMap.remove(); + this.state.mbMap = null; + } + this.props.onMapDestroyed(); + } + + _debouncedSync = _.debounce(() => { + if (this._isMounted && this.props.isMapReady) { + if (!this.state.hasSyncedLayerList) { + this.setState( + { + hasSyncedLayerList: true, + }, + () => { + this._syncMbMapWithLayerList(); + this._syncMbMapWithInspector(); + } + ); + } + this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap); + this._syncSettings(); + } + }, 256); + + _getMapState() { + const zoom = this.state.mbMap.getZoom(); + const mbCenter = this.state.mbMap.getCenter(); + const mbBounds = this.state.mbMap.getBounds(); + return { + zoom: _.round(zoom, ZOOM_PRECISION), + center: { + lon: _.round(mbCenter.lng, DECIMAL_DEGREES_PRECISION), + lat: _.round(mbCenter.lat, DECIMAL_DEGREES_PRECISION), + }, + extent: { + minLon: _.round(mbBounds.getWest(), DECIMAL_DEGREES_PRECISION), + minLat: _.round(mbBounds.getSouth(), DECIMAL_DEGREES_PRECISION), + maxLon: _.round(mbBounds.getEast(), DECIMAL_DEGREES_PRECISION), + maxLat: _.round(mbBounds.getNorth(), DECIMAL_DEGREES_PRECISION), + }, + }; + } + + async _createMbMapInstance() { + return new Promise(resolve => { + const mbStyle = { + version: 8, + sources: {}, + layers: [], + }; + const glyphUrl = getGlyphUrl(); + if (glyphUrl) { + mbStyle.glyphs = glyphUrl; + } + + const options = { + attributionControl: false, + container: this.refs.mapContainer, + style: mbStyle, + scrollZoom: this.props.scrollZoom, + preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false), + interactive: !this.props.disableInteractive, + maxZoom: this.props.settings.maxZoom, + minZoom: this.props.settings.minZoom, + }; + const initialView = _.get(this.props.goto, 'center'); + if (initialView) { + options.zoom = initialView.zoom; + options.center = { + lng: initialView.lon, + lat: initialView.lat, + }; + } else { + options.bounds = [-170, -60, 170, 75]; + } + const mbMap = new mapboxgl.Map(options); + mbMap.dragRotate.disable(); + mbMap.touchZoomRotate.disableRotation(); + if (!this.props.disableInteractive) { + mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); + } + + let emptyImage; + mbMap.on('styleimagemissing', e => { + if (emptyImage) { + mbMap.addImage(e.id, emptyImage); + } + }); + mbMap.on('load', () => { + emptyImage = new Image(); + + emptyImage.src = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; + emptyImage.crossOrigin = 'anonymous'; + resolve(mbMap); + }); + }); + } + + async _initializeMap() { + let mbMap; + try { + mbMap = await this._createMbMapInstance(); + } catch (error) { + this.props.setMapInitError(error.message); + return; + } + + if (!this._isMounted) { + return; + } + + this.setState({ mbMap }, () => { + this._loadMakiSprites(); + this._initResizerChecker(); + this._registerMapEventListeners(); + this.props.onMapReady(this._getMapState()); + }); + } + + _registerMapEventListeners() { + // moveend callback is debounced to avoid updating map extent state while map extent is still changing + // moveend is fired while the map extent is still changing in the following scenarios + // 1) During opening/closing of layer details panel, the EUI animation results in 8 moveend events + // 2) Setting map zoom and center from goto is done in 2 API calls, resulting in 2 moveend events + this.state.mbMap.on( + 'moveend', + _.debounce(() => { + this.props.extentChanged(this._getMapState()); + }, 100) + ); + // Attach event only if view control is visible, which shows lat/lon + if (!this.props.hideViewControl) { + const throttledSetMouseCoordinates = _.throttle(e => { + this.props.setMouseCoordinates({ + lat: e.lngLat.lat, + lon: e.lngLat.lng, + }); + }, 100); + this.state.mbMap.on('mousemove', throttledSetMouseCoordinates); + this.state.mbMap.on('mouseout', () => { + throttledSetMouseCoordinates.cancel(); // cancel any delayed setMouseCoordinates invocations + this.props.clearMouseCoordinates(); + }); + } + } + + _initResizerChecker() { + this._checker = new ResizeChecker(this.refs.mapContainer); + this._checker.on('resize', () => { + this.state.mbMap.resize(); + }); + } + + _loadMakiSprites() { + const sprites = isRetina() ? sprites2 : sprites1; + const json = isRetina() ? spritesheet[2] : spritesheet[1]; + addSpritesheetToMap(json, sprites, this.state.mbMap); + } + + _syncMbMapWithMapState = () => { + const { isMapReady, goto, clearGoto } = this.props; + + if (!isMapReady || !goto) { + return; + } + + clearGoto(); + + if (goto.bounds) { + //clamping ot -89/89 latitudes since Mapboxgl does not seem to handle bounds that contain the poles (logs errors to the console when using -90/90) + const lnLatBounds = new mapboxgl.LngLatBounds( + new mapboxgl.LngLat( + clampToLonBounds(goto.bounds.minLon), + clampToLatBounds(goto.bounds.minLat) + ), + new mapboxgl.LngLat( + clampToLonBounds(goto.bounds.maxLon), + clampToLatBounds(goto.bounds.maxLat) + ) + ); + //maxZoom ensure we're not zooming in too far on single points or small shapes + //the padding is to avoid too tight of a fit around edges + this.state.mbMap.fitBounds(lnLatBounds, { maxZoom: 17, padding: 16 }); + } else if (goto.center) { + this.state.mbMap.setZoom(goto.center.zoom); + this.state.mbMap.setCenter({ + lng: goto.center.lon, + lat: goto.center.lat, + }); + } + }; + + _syncMbMapWithLayerList = () => { + removeOrphanedSourcesAndLayers( + this.state.mbMap, + this.props.layerList, + this.props.spatialFiltersLayer + ); + this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap)); + syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList); + moveLayerToTop(this.state.mbMap, this.props.spatialFiltersLayer); + }; + + _syncMbMapWithInspector = () => { + if (!this.props.inspectorAdapters.map) { + return; + } + + const stats = { + center: this.state.mbMap.getCenter().toArray(), + zoom: this.state.mbMap.getZoom(), + }; + this.props.inspectorAdapters.map.setMapState({ + stats, + style: this.state.mbMap.getStyle(), + }); + }; + + _syncSettings() { + let zoomRangeChanged = false; + if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) { + this.state.mbMap.setMinZoom(this.props.settings.minZoom); + zoomRangeChanged = true; + } + if (this.props.settings.maxZoom !== this.state.mbMap.getMaxZoom()) { + this.state.mbMap.setMaxZoom(this.props.settings.maxZoom); + zoomRangeChanged = true; + } + + // 'moveend' event not fired when map moves from setMinZoom or setMaxZoom + // https://github.com/mapbox/mapbox-gl-js/issues/9610 + // hack to update extent after zoom update finishes moving map. + if (zoomRangeChanged) { + setTimeout(() => { + this.props.extentChanged(this._getMapState()); + }, 300); + } + } + + render() { + let drawControl; + let tooltipControl; + if (this.state.mbMap) { + drawControl = ; + tooltipControl = !this.props.disableTooltipControl ? ( + + ) : null; + } + return ( +
+ {drawControl} + {tooltipControl} +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts new file mode 100644 index 0000000000000..329fac28d7d2e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { FLYOUT_STATE } from '../../reducers/ui'; +import { MapStoreState } from '../../reducers/store'; +import { MapSettingsPanel } from './map_settings_panel'; +import { rollbackMapSettings, updateMapSetting } from '../../actions/map_actions'; +import { getMapSettings, hasMapSettingsChanges } from '../../selectors/map_selectors'; +import { updateFlyout } from '../../actions/ui_actions'; + +function mapStateToProps(state: MapStoreState) { + return { + settings: getMapSettings(state), + hasMapSettingsChanges: hasMapSettingsChanges(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + cancelChanges: () => { + dispatch(rollbackMapSettings()); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + }, + keepChanges: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + }, + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => { + dispatch(updateMapSetting(settingKey, settingValue)); + }, + }; +} + +const connectedMapSettingsPanel = connect(mapStateToProps, mapDispatchToProps)(MapSettingsPanel); +export { connectedMapSettingsPanel as MapSettingsPanel }; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx new file mode 100644 index 0000000000000..a89f4461fff06 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -0,0 +1,100 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { NavigationPanel } from './navigation_panel'; +import { SpatialFiltersPanel } from './spatial_filters_panel'; + +interface Props { + cancelChanges: () => void; + hasMapSettingsChanges: boolean; + keepChanges: () => void; + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function MapSettingsPanel({ + cancelChanges, + hasMapSettingsChanges, + keepChanges, + settings, + updateMapSetting, +}: Props) { + // TODO move common text like Cancel and Close to common i18n translation + const closeBtnLabel = hasMapSettingsChanges + ? i18n.translate('xpack.maps.mapSettingsPanel.cancelLabel', { + defaultMessage: 'Cancel', + }) + : i18n.translate('xpack.maps.mapSettingsPanel.closeLabel', { + defaultMessage: 'Close', + }); + + return ( + + + +

+ +

+
+
+ +
+
+ + + +
+
+ + + + + + {closeBtnLabel} + + + + + + + + + + + + +
+ ); +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx new file mode 100644 index 0000000000000..ed83e838f44f6 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { ValidatedDualRange, Value } from '../../../../../../src/plugins/kibana_react/public'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; + +interface Props { + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function NavigationPanel({ settings, updateMapSetting }: Props) { + const onZoomChange = (value: Value) => { + updateMapSetting('minZoom', Math.max(MIN_ZOOM, parseInt(value[0] as string, 10))); + updateMapSetting('maxZoom', Math.min(MAX_ZOOM, parseInt(value[1] as string, 10))); + }; + + return ( + + +
+ +
+
+ + + +
+ ); +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx new file mode 100644 index 0000000000000..cae703e982966 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx @@ -0,0 +1,98 @@ +/* + * 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 { EuiFormRow, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { AlphaSlider } from '../../components/alpha_slider'; +import { MbValidatedColorPicker } from '../../layers/styles/vector/components/color/mb_validated_color_picker'; + +interface Props { + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function SpatialFiltersPanel({ settings, updateMapSetting }: Props) { + const onAlphaChange = (alpha: number) => { + updateMapSetting('spatialFiltersAlpa', alpha); + }; + + const onFillColorChange = (color: string) => { + updateMapSetting('spatialFiltersFillColor', color); + }; + + const onLineColorChange = (color: string) => { + updateMapSetting('spatialFiltersLineColor', color); + }; + + const onShowSpatialFiltersChange = (event: EuiSwitchEvent) => { + updateMapSetting('showSpatialFilters', event.target.checked); + }; + + const renderStyleInputs = () => { + if (!settings.showSpatialFilters) { + return null; + } + + return ( + <> + + + + + + + + + + + ); + }; + + return ( + + +
+ +
+
+ + + + + + {renderStyleInputs()} +
+ ); +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss new file mode 100644 index 0000000000000..2754a3e204263 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss @@ -0,0 +1,21 @@ +@import 'tools_control/index'; + +.mapToolbarOverlay { + position: absolute; + top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin + left: $euiSizeM; + z-index: 2; // Sit on top of mapbox controls shadow +} + +.mapToolbarOverlay__button { + @include size($euiSizeXL); + // sass-lint:disable-block no-important + background-color: $euiColorEmptyShade !important; + pointer-events: all; + + &:enabled, + &:enabled:hover, + &:enabled:focus { + @include euiBottomShadowLarge; + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js new file mode 100644 index 0000000000000..c3cc4090ab952 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js @@ -0,0 +1,39 @@ +/* + * 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 { connect } from 'react-redux'; +import { SetViewControl } from './set_view_control'; +import { setGotoWithCenter } from '../../../actions/map_actions'; +import { getMapZoom, getMapCenter, getMapSettings } from '../../../selectors/map_selectors'; +import { closeSetView, openSetView } from '../../../actions/ui_actions'; +import { getIsSetViewOpen } from '../../../selectors/ui_selectors'; + +function mapStateToProps(state = {}) { + return { + settings: getMapSettings(state), + isSetViewOpen: getIsSetViewOpen(state), + zoom: getMapZoom(state), + center: getMapCenter(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + onSubmit: ({ lat, lon, zoom }) => { + dispatch(closeSetView()); + dispatch(setGotoWithCenter({ lat, lon, zoom })); + }, + closeSetView: () => { + dispatch(closeSetView()); + }, + openSetView: () => { + dispatch(openSetView()); + }, + }; +} + +const connectedSetViewControl = connect(mapStateToProps, mapDispatchToProps)(SetViewControl); +export { connectedSetViewControl as SetViewControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js similarity index 97% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js index 9c983447bfbf6..2c10728f78e5c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js @@ -18,7 +18,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; function getViewString(lat, lon, zoom) { return `${lat},${lon},${zoom}`; @@ -118,8 +117,8 @@ export class SetViewControl extends Component { const { isInvalid: isZoomInvalid, component: zoomFormRow } = this._renderNumberFormRow({ value: this.state.zoom, - min: MIN_ZOOM, - max: MAX_ZOOM, + min: this.props.settings.minZoom, + max: this.props.settings.maxZoom, onChange: this._onZoomChange, label: i18n.translate('xpack.maps.setViewControl.zoomLabel', { defaultMessage: 'Zoom', diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/_index.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/_index.scss new file mode 100644 index 0000000000000..5e5086bed2763 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/_index.scss @@ -0,0 +1,6 @@ +@import 'mixins'; + +@import 'widget_overlay'; +@import 'attribution_control/attribution_control'; +@import 'layer_control/index'; +@import 'view_control/view_control'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_mixins.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/_mixins.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_mixins.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/_mixins.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/index.js diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap new file mode 100644 index 0000000000000..0af4eb0793f03 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap @@ -0,0 +1,266 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LayerControl is rendered 1`] = ` + + + + + + +

+ +

+
+
+ + + + + +
+
+ + + +
+ + + + +
+`; + +exports[`LayerControl isLayerTOCOpen Should render expand button 1`] = ` + + + +`; + +exports[`LayerControl isLayerTOCOpen Should render expand button with error icon when layer has error 1`] = ` + + + +`; + +exports[`LayerControl isLayerTOCOpen Should render expand button with loading icon when layer is loading 1`] = ` + + + +`; + +exports[`LayerControl isReadOnly 1`] = ` + + + + + + +

+ +

+
+
+ + + + + +
+
+ + + +
+
+`; + +exports[`LayerControl should disable buttons when flyout is open 1`] = ` + + + + + + +

+ +

+
+
+ + + + + +
+
+ + + +
+ + + + +
+`; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss new file mode 100644 index 0000000000000..9a3e3a45d6c4e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss @@ -0,0 +1,2 @@ +@import 'layer_control'; +@import 'layer_toc/toc_entry/toc_entry'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js new file mode 100644 index 0000000000000..915f808b8e358 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js @@ -0,0 +1,45 @@ +/* + * 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 { connect } from 'react-redux'; +import { LayerControl } from './view'; + +import { FLYOUT_STATE } from '../../../reducers/ui'; +import { updateFlyout, setIsLayerTOCOpen } from '../../../actions/ui_actions'; +import { setSelectedLayer } from '../../../actions/map_actions'; +import { + getIsReadOnly, + getIsLayerTOCOpen, + getFlyoutDisplay, +} from '../../../selectors/ui_selectors'; +import { getLayerList } from '../../../selectors/map_selectors'; + +function mapStateToProps(state = {}) { + return { + isReadOnly: getIsReadOnly(state), + isLayerTOCOpen: getIsLayerTOCOpen(state), + layerList: getLayerList(state), + isFlyoutOpen: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + }; +} + +function mapDispatchToProps(dispatch) { + return { + showAddLayerWizard: async () => { + await dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.ADD_LAYER_WIZARD)); + }, + closeLayerTOC: () => { + dispatch(setIsLayerTOCOpen(false)); + }, + openLayerTOC: () => { + dispatch(setIsLayerTOCOpen(true)); + }, + }; +} + +const connectedLayerControl = connect(mapStateToProps, mapDispatchToProps)(LayerControl); +export { connectedLayerControl as LayerControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js new file mode 100644 index 0000000000000..ca3c6d325687d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js @@ -0,0 +1,69 @@ +/* + * 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 _ from 'lodash'; +import { connect } from 'react-redux'; +import { TOCEntry } from './view'; + +import { FLYOUT_STATE } from '../../../../../reducers/ui'; +import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions'; +import { getIsReadOnly, getOpenTOCDetails } from '../../../../../selectors/ui_selectors'; +import { + fitToLayerExtent, + setSelectedLayer, + toggleLayerVisible, + removeTransientLayer, + cloneLayer, + removeLayer, +} from '../../../../../actions/map_actions'; + +import { + hasDirtyState, + getSelectedLayer, + isUsingSearch, +} from '../../../../../selectors/map_selectors'; + +function mapStateToProps(state = {}, ownProps) { + return { + isReadOnly: getIsReadOnly(state), + zoom: _.get(state, 'map.mapState.zoom', 0), + selectedLayer: getSelectedLayer(state), + hasDirtyStateSelector: hasDirtyState(state), + isLegendDetailsOpen: getOpenTOCDetails(state).includes(ownProps.layer.getId()), + isUsingSearch: isUsingSearch(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + openLayerPanel: async layerId => { + await dispatch(removeTransientLayer()); + await dispatch(setSelectedLayer(layerId)); + dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); + }, + toggleVisible: layerId => { + dispatch(toggleLayerVisible(layerId)); + }, + fitToBounds: layerId => { + dispatch(fitToLayerExtent(layerId)); + }, + cloneLayer: layerId => { + dispatch(cloneLayer(layerId)); + }, + removeLayer: layerId => { + dispatch(removeLayer(layerId)); + }, + hideTOCDetails: layerId => { + dispatch(hideTOCDetails(layerId)); + }, + showTOCDetails: layerId => { + dispatch(showTOCDetails(layerId)); + }, + }; +} + +const connectedTOCEntry = connect(mapStateToProps, mapDispatchToProps)(TOCEntry); +export { connectedTOCEntry as TOCEntry }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js new file mode 100644 index 0000000000000..180dc2e3933c3 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js @@ -0,0 +1,162 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiButton, + EuiTitle, + EuiSpacer, + EuiButtonIcon, + EuiToolTip, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { LayerTOC } from './layer_toc'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +function renderExpandButton({ hasErrors, isLoading, onClick }) { + const expandLabel = i18n.translate('xpack.maps.layerControl.openLayerTOCButtonAriaLabel', { + defaultMessage: 'Expand layers panel', + }); + + if (isLoading) { + // Can not use EuiButtonIcon with spinner because spinner is a class and not an icon + return ( + + ); + } + + return ( + + ); +} + +export function LayerControl({ + isReadOnly, + isLayerTOCOpen, + showAddLayerWizard, + closeLayerTOC, + openLayerTOC, + layerList, + isFlyoutOpen, +}) { + if (!isLayerTOCOpen) { + const hasErrors = layerList.some(layer => { + return layer.hasErrors(); + }); + const isLoading = layerList.some(layer => { + return layer.isLayerLoading(); + }); + + return ( + + {renderExpandButton({ hasErrors, isLoading, onClick: openLayerTOC })} + + ); + } + + let addLayer; + if (!isReadOnly) { + addLayer = ( + + + + + + + ); + } + + return ( + + + + + + +

+ +

+
+
+ + + + + +
+
+ + + + +
+ + {addLayer} +
+ ); +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js similarity index 90% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js index ee5745efe5180..2f19b1b0c8c45 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js @@ -21,6 +21,7 @@ const defaultProps = { openLayerTOC: () => {}, isLayerTOCOpen: true, layerList: [], + isFlyoutOpen: false, }; describe('LayerControl', () => { @@ -30,6 +31,12 @@ describe('LayerControl', () => { expect(component).toMatchSnapshot(); }); + test('should disable buttons when flyout is open', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + test('isReadOnly', () => { const component = shallow(); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js index 417c5d84f8916..888fce7e7afe0 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js @@ -18,6 +18,7 @@ import { } from '../common/constants'; import { getEsSpatialRelationLabel } from '../common/i18n_getters'; import { SPATIAL_FILTER_TYPE } from './kibana_services'; +import turfCircle from '@turf/circle'; function ensureGeoField(type) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; @@ -330,7 +331,7 @@ export function createDistanceFilterWithMeta({ values: { distanceKm, geoFieldName, - pointLabel: point.join(','), + pointLabel: point.join(', '), }, }), }; @@ -451,3 +452,40 @@ export function clamp(val, min, max) { return val; } } + +export function extractFeaturesFromFilters(filters) { + const features = []; + filters + .filter(filter => { + return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE; + }) + .forEach(filter => { + let geometry; + if (filter.geo_distance && filter.geo_distance[filter.meta.key]) { + const distanceSplit = filter.geo_distance.distance.split('km'); + const distance = parseFloat(distanceSplit[0]); + const circleFeature = turfCircle(filter.geo_distance[filter.meta.key], distance); + geometry = circleFeature.geometry; + } else if ( + filter.geo_shape && + filter.geo_shape[filter.meta.key] && + filter.geo_shape[filter.meta.key].shape + ) { + geometry = filter.geo_shape[filter.meta.key].shape; + } else { + // do not know how to convert spatial filter to geometry + // this includes pre-indexed shapes + return; + } + + features.push({ + type: 'Feature', + geometry, + properties: { + filter: filter.meta.alias, + }, + }); + }); + + return features; +} diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js index fc02e19173843..d13291a8e2ba5 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -19,6 +19,7 @@ import { createExtentFilter, convertMapExtentToPolygon, roundCoordinates, + extractFeaturesFromFilters, } from './elasticsearch_geo_utils'; import { indexPatterns } from '../../../../src/plugins/data/public'; @@ -503,3 +504,131 @@ describe('roundCoordinates', () => { ]); }); }); + +describe('extractFeaturesFromFilters', () => { + it('should ignore non-spatial filers', () => { + const phraseFilter = { + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'machine.os', + negate: false, + params: { + query: 'ios', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'machine.os': 'ios', + }, + }, + }; + expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]); + }); + + it('should convert geo_distance filter to feature', () => { + const spatialFilter = { + geo_distance: { + distance: '1096km', + 'geo.coordinates': [-89.87125, 53.49454], + }, + meta: { + alias: 'geo.coordinates within 1096km of -89.87125,53.49454', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + const features = extractFeaturesFromFilters([spatialFilter]); + expect(features[0].geometry.coordinates[0][0]).toEqual([-89.87125, 63.35109118642093]); + expect(features[0].properties).toEqual({ + filter: 'geo.coordinates within 1096km of -89.87125,53.49454', + }); + }); + + it('should convert geo_shape filter to feature', () => { + const spatialFilter = { + geo_shape: { + 'geo.coordinates': { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + }, + ignore_unmapped: true, + }, + meta: { + alias: 'geo.coordinates in bounds', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + }, + properties: { + filter: 'geo.coordinates in bounds', + }, + }, + ]); + }); + + it('should ignore geo_shape filter with pre-index shape', () => { + const spatialFilter = { + geo_shape: { + 'geo.coordinates': { + indexed_shape: { + id: 's5gldXEBkTB2HMwpC8y0', + index: 'world_countries_v1', + path: 'coordinates', + }, + relation: 'INTERSECTS', + }, + ignore_unmapped: true, + }, + meta: { + alias: 'geo.coordinates in multipolygon', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/embeddable/README.md b/x-pack/plugins/maps/public/embeddable/README.md similarity index 100% rename from x-pack/legacy/plugins/maps/public/embeddable/README.md rename to x-pack/plugins/maps/public/embeddable/README.md diff --git a/x-pack/legacy/plugins/maps/public/embeddable/index.ts b/x-pack/plugins/maps/public/embeddable/index.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/embeddable/index.ts rename to x-pack/plugins/maps/public/embeddable/index.ts diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx similarity index 90% rename from x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx rename to x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index b8e4c84ad56a1..467cf4727edb7 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; import 'mapbox-gl/dist/mapbox-gl.css'; - -import { I18nContext } from 'ui/i18n'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { @@ -18,8 +16,8 @@ import { IContainer, EmbeddableInput, EmbeddableOutput, -} from '../../../../../../src/plugins/embeddable/public'; -import { APPLY_FILTER_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; +} from '../../../../../src/plugins/embeddable/public'; +import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; import { esFilters, IIndexPattern, @@ -27,11 +25,10 @@ import { Filter, Query, RefreshInterval, -} from '../../../../../../src/plugins/data/public'; - +} from '../../../../../src/plugins/data/public'; import { GisMap } from '../connected_components/gis_map'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createMapStore, MapStore } from '../../../../../plugins/maps/public/reducers/store'; +import { createMapStore, MapStore } from '../reducers/store'; +import { MapSettings } from '../reducers/map'; import { setGotoWithCenter, replaceLayerList, @@ -44,22 +41,20 @@ import { hideLayerControl, hideViewControl, setHiddenLayers, + setMapSettings, } from '../actions/map_actions'; -import { MapCenterAndZoom } from '../../../../../plugins/maps/common/descriptor_types'; +import { MapCenterAndZoom } from '../../common/descriptor_types'; import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { getInspectorAdapters, setEventHandlers, EventHandlers, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; +} from '../reducers/non_serializable_instances'; import { getMapCenter, getMapZoom, getHiddenLayerIds } from '../selectors/map_selectors'; import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderToolTipContent } from '../../../../../plugins/maps/public/layers/tooltips/tooltip_property'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiActions } from '../../../../../plugins/maps/public/kibana_services'; +import { RenderToolTipContent } from '../layers/tooltips/tooltip_property'; +import { getUiActions, getCoreI18n } from '../kibana_services'; interface MapEmbeddableConfig { editUrl?: string; @@ -67,6 +62,7 @@ interface MapEmbeddableConfig { editable: boolean; title?: string; layerList: unknown[]; + settings?: MapSettings; } export interface MapEmbeddableInput extends EmbeddableInput { @@ -104,6 +100,7 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input)); @@ -201,6 +199,10 @@ export class MapEmbeddable extends Embeddable diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts similarity index 80% rename from x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts rename to x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 96c3baf634a83..be6c3bf978d82 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -6,35 +6,23 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { npSetup, npStart } from 'ui/new_platform'; import { IIndexPattern } from 'src/plugins/data/public'; // @ts-ignore import { getMapsSavedObjectLoader } from '../angular/services/gis_map_saved_object_loader'; import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable'; -import { - getIndexPatternService, - getHttp, - getMapsCapabilities, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/kibana_services'; +import { getIndexPatternService, getHttp, getMapsCapabilities } from '../kibana_services'; import { EmbeddableFactoryDefinition, IContainer, -} from '../../../../../../src/plugins/embeddable/public'; +} from '../../../../../src/plugins/embeddable/public'; import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; + +import { createMapStore } from '../reducers/store'; import { addLayerWithoutDataSync } from '../actions/map_actions'; import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; import { getInitialLayers } from '../angular/get_initial_layers'; import { mergeInputWithSavedMap } from './merge_input_with_saved_map'; -import '../angular/services/gis_map_saved_object_loader'; -// @ts-ignore -import { - bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, - bindStartCoreAndPlugins as bindNpStartCoreAndPlugins, -} from '../../../../../plugins/maps/public/plugin'; // eslint-disable-line @kbn/eslint/no-restricted-paths export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; @@ -45,11 +33,6 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type: MAP_SAVED_OBJECT_TYPE, getIconForSavedObject: () => APP_ICON, }; - constructor() { - // Init required services. Necessary while in legacy - bindNpSetupCoreAndPlugins(npSetup.core, npSetup.plugins); - bindNpStartCoreAndPlugins(npStart.core, npStart.plugins); - } async isEditable() { return getMapsCapabilities().save as boolean; @@ -110,6 +93,14 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { const layerList = getInitialLayers(savedMap.layerListJSON); const indexPatterns = await this._getIndexPatterns(layerList); + let settings; + if (savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); + if (mapState.settings) { + settings = mapState.settings; + } + } + const embeddable = new MapEmbeddable( { layerList, @@ -117,6 +108,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { editUrl: getHttp().basePath.prepend(createMapPath(savedObjectId)), indexPatterns, editable: await this.isEditable(), + settings, }, input, parent @@ -151,8 +143,3 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { ); }; } - -npSetup.plugins.embeddable.registerEmbeddableFactory( - MAP_SAVED_OBJECT_TYPE, - new MapEmbeddableFactory() -); diff --git a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts rename to x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts diff --git a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js similarity index 89% rename from x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js rename to x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js index 8e3e0a9168e30..d91c91b3b223c 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js +++ b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js @@ -5,8 +5,8 @@ */ import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../../../plugins/maps/public/reducers/ui'; + +import { DEFAULT_IS_LAYER_TOC_OPEN } from '../reducers/ui'; const MAP_EMBEDDABLE_INPUT_KEYS = [ 'hideFilterActions', diff --git a/x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts b/x-pack/plugins/maps/public/feature_catalogue_entry.ts similarity index 89% rename from x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts rename to x-pack/plugins/maps/public/feature_catalogue_entry.ts index fdda76b4e1212..6c2579bd3e4e2 100644 --- a/x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts +++ b/x-pack/plugins/maps/public/feature_catalogue_entry.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { APP_ID, APP_ICON } from '../common/constants'; import { getAppTitle } from '../common/i18n_getters'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; export const featureCatalogueEntry = { id: APP_ID, diff --git a/x-pack/legacy/plugins/maps/public/help_menu_util.js b/x-pack/plugins/maps/public/help_menu_util.js similarity index 81% rename from x-pack/legacy/plugins/maps/public/help_menu_util.js rename to x-pack/plugins/maps/public/help_menu_util.js index 70b9340b562cd..053caf6688309 100644 --- a/x-pack/legacy/plugins/maps/public/help_menu_util.js +++ b/x-pack/plugins/maps/public/help_menu_util.js @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getDocLinks, getCoreChrome } from '../../../../plugins/maps/public/kibana_services'; + +import { getDocLinks, getCoreChrome } from './kibana_services'; export function addHelpMenuToAppChrome() { const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); diff --git a/x-pack/legacy/plugins/maps/public/icon.svg b/x-pack/plugins/maps/public/icon.svg similarity index 100% rename from x-pack/legacy/plugins/maps/public/icon.svg rename to x-pack/plugins/maps/public/icon.svg diff --git a/x-pack/plugins/maps/public/index.scss b/x-pack/plugins/maps/public/index.scss new file mode 100644 index 0000000000000..8b2f6d3cb6156 --- /dev/null +++ b/x-pack/plugins/maps/public/index.scss @@ -0,0 +1,17 @@ +/* GIS plugin styles */ + +// Import the EUI global scope so we can use EUI constants +@import 'src/legacy/ui/public/styles/_styling_constants'; + +// Prefix all styles with "map" to avoid conflicts. +// Examples +// mapChart +// mapChart__legend +// mapChart__legend--small +// mapChart__legend-isLoading + +@import 'main'; +@import 'mapbox_hacks'; +@import 'connected_components/index'; +@import 'components/index'; +@import 'layers/index'; diff --git a/x-pack/plugins/maps/public/kibana_services.d.ts b/x-pack/plugins/maps/public/kibana_services.d.ts index 867557c296292..3d346fe1acdd5 100644 --- a/x-pack/plugins/maps/public/kibana_services.d.ts +++ b/x-pack/plugins/maps/public/kibana_services.d.ts @@ -3,8 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { IIndexPattern } from 'src/plugins/data/public'; +import { IIndexPattern, DataPublicPluginStart } from 'src/plugins/data/public'; export function getLicenseId(): any; export function getInspector(): any; @@ -30,6 +29,7 @@ export function getUiActions(): any; export function getCore(): any; export function getNavigation(): any; export function getCoreI18n(): any; +export function getSearchService(): DataPublicPluginStart['search']; export function setLicenseId(args: unknown): void; export function setInspector(args: unknown): void; @@ -53,3 +53,4 @@ export function setUiActions(args: unknown): void; export function setCore(args: unknown): void; export function setNavigation(args: unknown): void; export function setCoreI18n(args: unknown): void; +export function setSearchService(args: DataPublicPluginStart['search']): void; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index dcbd54a09381f..431d7a3b339b7 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -5,8 +5,6 @@ */ import { esFilters, search } from '../../../../src/plugins/data/public'; -export { SearchSource } from '../../../../src/plugins/data/public'; - export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; const { getRequestInspectorStats, getResponseInspectorStats } = search; @@ -137,3 +135,7 @@ export const getNavigation = () => navigation; let coreI18n; export const setCoreI18n = kibanaCoreI18n => (coreI18n = kibanaCoreI18n); export const getCoreI18n = () => coreI18n; + +let dataSearchService; +export const setSearchService = searchService => (dataSearchService = searchService); +export const getSearchService = () => dataSearchService; diff --git a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts index 9a9ea2968ceeb..1fc3ad203706f 100644 --- a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts @@ -24,7 +24,7 @@ import { } from '../../common/constants'; import { ESGeoGridSource } from './sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from './util/can_skip_fetch'; -import { IVectorLayer, VectorLayerArguments } from './vector_layer'; +import { IVectorLayer } from './vector_layer'; import { IESSource } from './sources/es_source'; import { IESAggSource } from './sources/es_agg_source'; import { ISource } from './sources/source'; @@ -36,6 +36,8 @@ import { DynamicStylePropertyOptions, VectorLayerDescriptor, } from '../../common/descriptor_types'; +import { IStyle } from './styles/style'; +import { IVectorSource } from './sources/vector_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; @@ -145,6 +147,11 @@ function getClusterStyleDescriptor( return clusterStyleDescriptor; } +export interface BlendedVectorLayerArguments { + source: IVectorSource; + layerDescriptor: VectorLayerDescriptor; +} + export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { static type = LAYER_TYPE.BLENDED_VECTOR; @@ -163,11 +170,14 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { private readonly _documentSource: IESSource; private readonly _documentStyle: IVectorStyle; - constructor(options: VectorLayerArguments) { - super(options); + constructor(options: BlendedVectorLayerArguments) { + super({ + ...options, + joins: [], + }); this._documentSource = this._source as IESSource; // VectorLayer constructor sets _source as document source - this._documentStyle = this._style; // VectorLayer constructor sets _style as document source + this._documentStyle = this._style as IVectorStyle; // VectorLayer constructor sets _style as document source this._clusterSource = getClusterSource(this._documentSource, this._documentStyle); const clusterStyleDescriptor = getClusterStyleDescriptor( @@ -229,11 +239,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { return this._documentSource; } - getCurrentStyle() { + getCurrentStyle(): IStyle { return this._isClustered ? this._clusterStyle : this._documentStyle; } - getStyleForEditing() { + getStyleForEditing(): IStyle { return this._documentStyle; } @@ -242,8 +252,8 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { const requestToken = Symbol(`layer-active-count:${this.getId()}`); const searchFilters = this._getSearchFilters( syncContext.dataFilters, - this.getSource(), - this.getCurrentStyle() + this.getSource() as IVectorSource, + this.getCurrentStyle() as IVectorStyle ); const canSkipFetch = await canSkipSourceUpdate({ source: this.getSource(), diff --git a/x-pack/plugins/maps/public/layers/layer.d.ts b/x-pack/plugins/maps/public/layers/layer.d.ts deleted file mode 100644 index e8fc5d473626c..0000000000000 --- a/x-pack/plugins/maps/public/layers/layer.d.ts +++ /dev/null @@ -1,51 +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 { LayerDescriptor, MapExtent, MapFilters, MapQuery } from '../../common/descriptor_types'; -import { ISource } from './sources/source'; -import { DataRequest } from './util/data_request'; -import { SyncContext } from '../actions/map_actions'; - -export interface ILayer { - getBounds(mapFilters: MapFilters): Promise; - getDataRequest(id: string): DataRequest | undefined; - getDisplayName(source?: ISource): Promise; - getId(): string; - getSourceDataRequest(): DataRequest | undefined; - getSource(): ISource; - getSourceForEditing(): ISource; - syncData(syncContext: SyncContext): Promise; - isVisible(): boolean; - showAtZoomLevel(zoomLevel: number): boolean; - getMinZoom(): number; - getMaxZoom(): number; - getMinSourceZoom(): number; -} - -export interface ILayerArguments { - layerDescriptor: LayerDescriptor; - source: ISource; -} - -export class AbstractLayer implements ILayer { - static createDescriptor(options: Partial, mapColors?: string[]): LayerDescriptor; - constructor(layerArguments: ILayerArguments); - getBounds(mapFilters: MapFilters): Promise; - getDataRequest(id: string): DataRequest | undefined; - getDisplayName(source?: ISource): Promise; - getId(): string; - getSourceDataRequest(): DataRequest | undefined; - getSource(): ISource; - getSourceForEditing(): ISource; - syncData(syncContext: SyncContext): Promise; - isVisible(): boolean; - showAtZoomLevel(zoomLevel: number): boolean; - getMinZoom(): number; - getMaxZoom(): number; - getMinSourceZoom(): number; - getQuery(): MapQuery; - _removeStaleMbSourcesAndLayers(mbMap: unknown): void; - _requiresPrevSourceCleanup(mbMap: unknown): boolean; -} diff --git a/x-pack/plugins/maps/public/layers/layer.js b/x-pack/plugins/maps/public/layers/layer.js deleted file mode 100644 index 19dcbaf1dfcfd..0000000000000 --- a/x-pack/plugins/maps/public/layers/layer.js +++ /dev/null @@ -1,373 +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 _ from 'lodash'; -import React from 'react'; -import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import { DataRequest } from './util/data_request'; -import { - MAX_ZOOM, - MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, - MIN_ZOOM, - SOURCE_DATA_ID_ORIGIN, -} from '../../common/constants'; -import uuid from 'uuid/v4'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../reducers/util.js'; -import { i18n } from '@kbn/i18n'; - -export class AbstractLayer { - constructor({ layerDescriptor, source }) { - this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); - this._source = source; - if (this._descriptor.__dataRequests) { - this._dataRequests = this._descriptor.__dataRequests.map( - dataRequest => new DataRequest(dataRequest) - ); - } else { - this._dataRequests = []; - } - } - - static getBoundDataForSource(mbMap, sourceId) { - const mbStyle = mbMap.getStyle(); - return mbStyle.sources[sourceId].data; - } - - static createDescriptor(options = {}) { - const layerDescriptor = { ...options }; - - layerDescriptor.__dataRequests = _.get(options, '__dataRequests', []); - layerDescriptor.id = _.get(options, 'id', uuid()); - layerDescriptor.label = options.label && options.label.length > 0 ? options.label : null; - layerDescriptor.minZoom = _.get(options, 'minZoom', MIN_ZOOM); - layerDescriptor.maxZoom = _.get(options, 'maxZoom', MAX_ZOOM); - layerDescriptor.alpha = _.get(options, 'alpha', 0.75); - layerDescriptor.visible = _.get(options, 'visible', true); - layerDescriptor.style = _.get(options, 'style', {}); - - return layerDescriptor; - } - - destroy() { - if (this._source) { - this._source.destroy(); - } - } - - async cloneDescriptor() { - const clonedDescriptor = copyPersistentState(this._descriptor); - // layer id is uuid used to track styles/layers in mapbox - clonedDescriptor.id = uuid(); - const displayName = await this.getDisplayName(); - clonedDescriptor.label = `Clone of ${displayName}`; - clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); - if (clonedDescriptor.joins) { - clonedDescriptor.joins.forEach(joinDescriptor => { - // right.id is uuid used to track requests in inspector - joinDescriptor.right.id = uuid(); - }); - } - return clonedDescriptor; - } - - makeMbLayerId(layerNameSuffix) { - return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; - } - - isJoinable() { - return this.getSource().isJoinable(); - } - - supportsElasticsearchFilters() { - return this.getSource().isESSource(); - } - - async supportsFitToBounds() { - return await this.getSource().supportsFitToBounds(); - } - - async getDisplayName(source) { - if (this._descriptor.label) { - return this._descriptor.label; - } - - const sourceDisplayName = source - ? await source.getDisplayName() - : await this.getSource().getDisplayName(); - return sourceDisplayName || `Layer ${this._descriptor.id}`; - } - - async getAttributions() { - if (!this.hasErrors()) { - return await this.getSource().getAttributions(); - } - return []; - } - - getLabel() { - return this._descriptor.label ? this._descriptor.label : ''; - } - - getCustomIconAndTooltipContent() { - return { - icon: , - }; - } - - getIconAndTooltipContent(zoomLevel, isUsingSearch) { - let icon; - let tooltipContent = null; - const footnotes = []; - if (this.hasErrors()) { - icon = ( - - ); - tooltipContent = this.getErrors(); - } else if (this.isLayerLoading()) { - icon = ; - } else if (!this.isVisible()) { - icon = ; - tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { - defaultMessage: `Layer is hidden.`, - }); - } else if (!this.showAtZoomLevel(zoomLevel)) { - const minZoom = this.getMinZoom(); - const maxZoom = this.getMaxZoom(); - icon = ; - tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { - defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, - values: { minZoom, maxZoom }, - }); - } else { - const customIconAndTooltipContent = this.getCustomIconAndTooltipContent(); - if (customIconAndTooltipContent) { - icon = customIconAndTooltipContent.icon; - if (!customIconAndTooltipContent.areResultsTrimmed) { - tooltipContent = customIconAndTooltipContent.tooltipContent; - } else { - footnotes.push({ - icon: , - message: customIconAndTooltipContent.tooltipContent, - }); - } - } - - if (isUsingSearch && this.getQueryableIndexPatternIds().length) { - footnotes.push({ - icon: , - message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { - defaultMessage: 'Results narrowed by search bar', - }), - }); - } - } - - return { - icon, - tooltipContent, - footnotes, - }; - } - - async hasLegendDetails() { - return false; - } - - renderLegendDetails() { - return null; - } - - getId() { - return this._descriptor.id; - } - - getSource() { - return this._source; - } - - getSourceForEditing() { - return this._source; - } - - isVisible() { - return this._descriptor.visible; - } - - showAtZoomLevel(zoom) { - return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); - } - - getMinZoom() { - return this._descriptor.minZoom; - } - - getMaxZoom() { - return this._descriptor.maxZoom; - } - - getMinSourceZoom() { - return this._source.getMinZoom(); - } - - _requiresPrevSourceCleanup() { - return false; - } - - _removeStaleMbSourcesAndLayers(mbMap) { - if (this._requiresPrevSourceCleanup(mbMap)) { - const mbStyle = mbMap.getStyle(); - mbStyle.layers.forEach(mbLayer => { - if (this.ownsMbLayerId(mbLayer.id)) { - mbMap.removeLayer(mbLayer.id); - } - }); - Object.keys(mbStyle.sources).some(mbSourceId => { - if (this.ownsMbSourceId(mbSourceId)) { - mbMap.removeSource(mbSourceId); - } - }); - } - } - - getAlpha() { - return this._descriptor.alpha; - } - - getQuery() { - return this._descriptor.query; - } - - getCurrentStyle() { - return this._style; - } - - getStyleForEditing() { - return this._style; - } - - async getImmutableSourceProperties() { - return this.getSource().getImmutableProperties(); - } - - renderSourceSettingsEditor = ({ onChange }) => { - return this.getSourceForEditing().renderSourceSettingsEditor({ onChange }); - }; - - getPrevRequestToken(dataId) { - const prevDataRequest = this.getDataRequest(dataId); - if (!prevDataRequest) { - return; - } - - return prevDataRequest.getRequestToken(); - } - - getInFlightRequestTokens() { - if (!this._dataRequests) { - return []; - } - - const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken()); - return _.compact(requestTokens); - } - - getSourceDataRequest() { - return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); - } - - getDataRequest(id) { - return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id); - } - - isLayerLoading() { - return this._dataRequests.some(dataRequest => dataRequest.isLoading()); - } - - hasErrors() { - return _.get(this._descriptor, '__isInErrorState', false); - } - - getErrors() { - return this.hasErrors() ? this._descriptor.__errorMessage : ''; - } - - toLayerDescriptor() { - return this._descriptor; - } - - async syncData() { - //no-op by default - } - - getMbLayerIds() { - throw new Error('Should implement AbstractLayer#getMbLayerIds'); - } - - ownsMbLayerId() { - throw new Error('Should implement AbstractLayer#ownsMbLayerId'); - } - - ownsMbSourceId() { - throw new Error('Should implement AbstractLayer#ownsMbSourceId'); - } - - canShowTooltip() { - return false; - } - - syncLayerWithMB() { - throw new Error('Should implement AbstractLayer#syncLayerWithMB'); - } - - getLayerTypeIconName() { - throw new Error('should implement Layer#getLayerTypeIconName'); - } - - isDataLoaded() { - const sourceDataRequest = this.getSourceDataRequest(); - return sourceDataRequest && sourceDataRequest.hasData(); - } - - async getBounds(/* mapFilters: MapFilters */) { - return { - minLon: -180, - maxLon: 180, - minLat: -89, - maxLat: 89, - }; - } - - renderStyleEditor({ onStyleDescriptorChange }) { - const style = this.getStyleForEditing(); - if (!style) { - return null; - } - return style.renderEditor({ layer: this, onStyleDescriptorChange }); - } - - getIndexPatternIds() { - return []; - } - - getQueryableIndexPatternIds() { - return []; - } - - syncVisibilityWithMb(mbMap, mbLayerId) { - mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); - } - - getType() { - return this._descriptor.type; - } -} diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx new file mode 100644 index 0000000000000..ce48793e1481b --- /dev/null +++ b/x-pack/plugins/maps/public/layers/layer.tsx @@ -0,0 +1,490 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { Query } from 'src/plugins/data/public'; +import _ from 'lodash'; +import React, { ReactElement } from 'react'; +import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import uuid from 'uuid/v4'; +import { i18n } from '@kbn/i18n'; +import { FeatureCollection } from 'geojson'; +import { DataRequest } from './util/data_request'; +import { + MAX_ZOOM, + MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, + MIN_ZOOM, + SOURCE_DATA_ID_ORIGIN, +} from '../../common/constants'; +// @ts-ignore +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../reducers/util.js'; +import { + LayerDescriptor, + MapExtent, + MapFilters, + StyleDescriptor, +} from '../../common/descriptor_types'; +import { Attribution, ImmutableSourceProperty, ISource } from './sources/source'; +import { SyncContext } from '../actions/map_actions'; +import { IStyle } from './styles/style'; + +export interface ILayer { + getBounds(mapFilters: MapFilters): Promise; + getDataRequest(id: string): DataRequest | undefined; + getDisplayName(source?: ISource): Promise; + getId(): string; + getSourceDataRequest(): DataRequest | undefined; + getSource(): ISource; + getSourceForEditing(): ISource; + syncData(syncContext: SyncContext): void; + supportsElasticsearchFilters(): boolean; + supportsFitToBounds(): Promise; + getAttributions(): Promise; + getLabel(): string; + getCustomIconAndTooltipContent(): IconAndTooltipContent; + getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent; + renderLegendDetails(): ReactElement | null; + showAtZoomLevel(zoom: number): boolean; + getMinZoom(): number; + getMaxZoom(): number; + getMinSourceZoom(): number; + getAlpha(): number; + getQuery(): Query | null; + getStyle(): IStyle; + getStyleForEditing(): IStyle; + getCurrentStyle(): IStyle; + getImmutableSourceProperties(): Promise; + renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null; + isLayerLoading(): boolean; + hasErrors(): boolean; + getErrors(): string; + toLayerDescriptor(): LayerDescriptor; + getMbLayerIds(): string[]; + ownsMbLayerId(mbLayerId: string): boolean; + ownsMbSourceId(mbSourceId: string): boolean; + canShowTooltip(): boolean; + syncLayerWithMB(mbMap: unknown): void; + getLayerTypeIconName(): string; + isDataLoaded(): boolean; + getIndexPatternIds(): string[]; + getQueryableIndexPatternIds(): string[]; + getType(): string | undefined; + isVisible(): boolean; + cloneDescriptor(): Promise; + renderStyleEditor({ + onStyleDescriptorChange, + }: { + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement | null; +} +export type Footnote = { + icon: ReactElement; + message?: string | null; +}; +export type IconAndTooltipContent = { + icon?: ReactElement | null; + tooltipContent?: string | null; + footnotes?: Footnote[] | null; + areResultsTrimmed?: boolean; +}; + +export interface ILayerArguments { + layerDescriptor: LayerDescriptor; + source: ISource; + style: IStyle; +} + +export class AbstractLayer implements ILayer { + protected readonly _descriptor: LayerDescriptor; + protected readonly _source: ISource; + protected readonly _style: IStyle; + protected readonly _dataRequests: DataRequest[]; + + static createDescriptor(options: Partial): LayerDescriptor { + return { + ...options, + sourceDescriptor: options.sourceDescriptor ? options.sourceDescriptor : null, + __dataRequests: _.get(options, '__dataRequests', []), + id: _.get(options, 'id', uuid()), + label: options.label && options.label.length > 0 ? options.label : null, + minZoom: _.get(options, 'minZoom', MIN_ZOOM), + maxZoom: _.get(options, 'maxZoom', MAX_ZOOM), + alpha: _.get(options, 'alpha', 0.75), + visible: _.get(options, 'visible', true), + style: _.get(options, 'style', null), + }; + } + + destroy() { + if (this._source) { + this._source.destroy(); + } + } + + constructor({ layerDescriptor, source, style }: ILayerArguments) { + this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); + this._source = source; + this._style = style; + if (this._descriptor.__dataRequests) { + this._dataRequests = this._descriptor.__dataRequests.map( + dataRequest => new DataRequest(dataRequest) + ); + } else { + this._dataRequests = []; + } + } + + static getBoundDataForSource(mbMap: unknown, sourceId: string): FeatureCollection { + // @ts-ignore + const mbStyle = mbMap.getStyle(); + return mbStyle.sources[sourceId].data; + } + + async cloneDescriptor(): Promise { + // @ts-ignore + const clonedDescriptor = copyPersistentState(this._descriptor); + // layer id is uuid used to track styles/layers in mapbox + clonedDescriptor.id = uuid(); + const displayName = await this.getDisplayName(); + clonedDescriptor.label = `Clone of ${displayName}`; + clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); + + // todo: remove this + // This should not be in AbstractLayer. It relies on knowledge of VectorLayerDescriptor + // @ts-ignore + if (clonedDescriptor.joins) { + // @ts-ignore + clonedDescriptor.joins.forEach(joinDescriptor => { + // right.id is uuid used to track requests in inspector + // @ts-ignore + joinDescriptor.right.id = uuid(); + }); + } + return clonedDescriptor; + } + + makeMbLayerId(layerNameSuffix: string): string { + return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; + } + + isJoinable(): boolean { + return this.getSource().isJoinable(); + } + + supportsElasticsearchFilters(): boolean { + return this.getSource().isESSource(); + } + + async supportsFitToBounds(): Promise { + return await this.getSource().supportsFitToBounds(); + } + + async getDisplayName(source?: ISource): Promise { + if (this._descriptor.label) { + return this._descriptor.label; + } + + const sourceDisplayName = source + ? await source.getDisplayName() + : await this.getSource().getDisplayName(); + return sourceDisplayName || `Layer ${this._descriptor.id}`; + } + + async getAttributions(): Promise { + if (!this.hasErrors()) { + return await this.getSource().getAttributions(); + } + return []; + } + + getStyleForEditing(): IStyle { + return this._style; + } + + getStyle() { + return this._style; + } + + getLabel(): string { + return this._descriptor.label ? this._descriptor.label : ''; + } + + getCustomIconAndTooltipContent(): IconAndTooltipContent { + return { + icon: , + }; + } + + getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent { + let icon; + let tooltipContent = null; + const footnotes = []; + if (this.hasErrors()) { + icon = ( + + ); + tooltipContent = this.getErrors(); + } else if (this.isLayerLoading()) { + icon = ; + } else if (!this.isVisible()) { + icon = ; + tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { + defaultMessage: `Layer is hidden.`, + }); + } else if (!this.showAtZoomLevel(zoomLevel)) { + const minZoom = this.getMinZoom(); + const maxZoom = this.getMaxZoom(); + icon = ; + tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { + defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, + values: { minZoom, maxZoom }, + }); + } else { + const customIconAndTooltipContent = this.getCustomIconAndTooltipContent(); + if (customIconAndTooltipContent) { + icon = customIconAndTooltipContent.icon; + if (!customIconAndTooltipContent.areResultsTrimmed) { + tooltipContent = customIconAndTooltipContent.tooltipContent; + } else { + footnotes.push({ + icon: , + message: customIconAndTooltipContent.tooltipContent, + }); + } + } + + if (isUsingSearch && this.getQueryableIndexPatternIds().length) { + footnotes.push({ + icon: , + message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { + defaultMessage: 'Results narrowed by search bar', + }), + }); + } + } + + return { + icon, + tooltipContent, + footnotes, + }; + } + + async hasLegendDetails(): Promise { + return false; + } + + renderLegendDetails(): ReactElement | null { + return null; + } + + getId(): string { + return this._descriptor.id; + } + + getSource(): ISource { + return this._source; + } + + getSourceForEditing(): ISource { + return this._source; + } + + isVisible(): boolean { + return !!this._descriptor.visible; + } + + showAtZoomLevel(zoom: number): boolean { + return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); + } + + getMinZoom(): number { + return typeof this._descriptor.minZoom === 'number' ? this._descriptor.minZoom : MIN_ZOOM; + } + + getMaxZoom(): number { + return typeof this._descriptor.maxZoom === 'number' ? this._descriptor.maxZoom : MAX_ZOOM; + } + + getMinSourceZoom(): number { + return this._source.getMinZoom(); + } + + _requiresPrevSourceCleanup(mbMap: unknown) { + return false; + } + + _removeStaleMbSourcesAndLayers(mbMap: unknown) { + if (this._requiresPrevSourceCleanup(mbMap)) { + // @ts-ignore + const mbStyle = mbMap.getStyle(); + // @ts-ignore + mbStyle.layers.forEach(mbLayer => { + // @ts-ignore + if (this.ownsMbLayerId(mbLayer.id)) { + // @ts-ignore + mbMap.removeLayer(mbLayer.id); + } + }); + // @ts-ignore + Object.keys(mbStyle.sources).some(mbSourceId => { + // @ts-ignore + if (this.ownsMbSourceId(mbSourceId)) { + // @ts-ignore + mbMap.removeSource(mbSourceId); + } + }); + } + } + + getAlpha(): number { + return typeof this._descriptor.alpha === 'number' ? this._descriptor.alpha : 1; + } + + getQuery(): Query | null { + return this._descriptor.query ? this._descriptor.query : null; + } + + getCurrentStyle(): IStyle { + return this._style; + } + + async getImmutableSourceProperties() { + const source = this.getSource(); + return await source.getImmutableProperties(); + } + + renderSourceSettingsEditor({ onChange }: { onChange: () => void }) { + const source = this.getSourceForEditing(); + return source.renderSourceSettingsEditor({ onChange }); + } + + getPrevRequestToken(dataId: string): symbol | undefined { + const prevDataRequest = this.getDataRequest(dataId); + if (!prevDataRequest) { + return; + } + + return prevDataRequest.getRequestToken(); + } + + getInFlightRequestTokens(): symbol[] { + if (!this._dataRequests) { + return []; + } + + const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken()); + + // Compact removes all the undefineds + // @ts-ignore + return _.compact(requestTokens); + } + + getSourceDataRequest(): DataRequest | undefined { + return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); + } + + getDataRequest(id: string): DataRequest | undefined { + return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id); + } + + isLayerLoading(): boolean { + return this._dataRequests.some(dataRequest => dataRequest.isLoading()); + } + + hasErrors(): boolean { + return _.get(this._descriptor, '__isInErrorState', false); + } + + getErrors(): string { + return this.hasErrors() && this._descriptor.__errorMessage + ? this._descriptor.__errorMessage + : ''; + } + + toLayerDescriptor(): LayerDescriptor { + return this._descriptor; + } + + async syncData(syncContext: SyncContext) { + // no-op by default + } + + getMbLayerIds(): string[] { + throw new Error('Should implement AbstractLayer#getMbLayerIds'); + } + + ownsMbLayerId(layerId: string): boolean { + throw new Error('Should implement AbstractLayer#ownsMbLayerId'); + } + + ownsMbSourceId(sourceId: string): boolean { + throw new Error('Should implement AbstractLayer#ownsMbSourceId'); + } + + canShowTooltip() { + return false; + } + + syncLayerWithMB(mbMap: unknown) { + throw new Error('Should implement AbstractLayer#syncLayerWithMB'); + } + + getLayerTypeIconName(): string { + throw new Error('should implement Layer#getLayerTypeIconName'); + } + + isDataLoaded(): boolean { + const sourceDataRequest = this.getSourceDataRequest(); + return sourceDataRequest ? sourceDataRequest.hasData() : false; + } + + async getBounds(mapFilters: MapFilters): Promise { + return { + minLon: -180, + maxLon: 180, + minLat: -89, + maxLat: 89, + }; + } + + renderStyleEditor({ + onStyleDescriptorChange, + }: { + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement | null { + const style = this.getStyleForEditing(); + if (!style) { + return null; + } + return style.renderEditor({ layer: this, onStyleDescriptorChange }); + } + + getIndexPatternIds(): string[] { + return []; + } + + getQueryableIndexPatternIds(): string[] { + return []; + } + + syncVisibilityWithMb(mbMap: unknown, mbLayerId: string) { + // @ts-ignore + mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); + } + + getType(): string | undefined { + return this._descriptor.type; + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 137513ad7c612..36f898f723757 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -21,8 +21,6 @@ import { registerSource } from '../source_registry'; export class GeojsonFileSource extends AbstractVectorSource { static type = SOURCE_TYPES.GEOJSON_FILE; - static isIndexingSource = true; - static createDescriptor(geoJson, name) { // Wrap feature as feature collection if needed let featureCollection; @@ -70,7 +68,7 @@ export class GeojsonFileSource extends AbstractVectorSource { } shouldBeIndexed() { - return GeojsonFileSource.isIndexingSource; + return true; } } diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 96347c444dd5b..51ee15e7ea5af 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -6,7 +6,7 @@ import { AbstractESAggSource } from '../es_agg_source'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; -import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; +import { GRID_RESOLUTION } from '../../../../common/constants'; export class ESGeoGridSource extends AbstractESAggSource { static createDescriptor({ @@ -14,12 +14,7 @@ export class ESGeoGridSource extends AbstractESAggSource { geoField, requestType, resolution, - }: { - indexPatternId: string; - geoField: string; - requestType: RENDER_AS; - resolution?: GRID_RESOLUTION; - }): ESGeoGridSourceDescriptor; + }: Partial): ESGeoGridSourceDescriptor; constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 34fed37933e13..bfbcca1eb3f61 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -10,7 +10,7 @@ import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { AbstractESSource } from '../es_source'; -import { SearchSource } from '../../../kibana_services'; +import { getSearchService } from '../../../kibana_services'; import { VectorStyle } from '../../styles/vector/vector_style'; import { VectorLayer } from '../../vector_layer'; import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; @@ -427,13 +427,17 @@ export class ESSearchSource extends AbstractESSource { return {}; } - const searchSource = new SearchSource(); + const searchService = getSearchService(); + const searchSource = searchService.searchSource.create(); + searchSource.setField('index', indexPattern); searchSource.setField('size', 1); + const query = { language: 'kuery', query: `_id:"${docId}" and _index:"${index}"`, }; + searchSource.setField('query', query); searchSource.setField('fields', this._getTooltipPropertyNames()); diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts index 092dc3bf0d5a8..d95ec5a64e6c3 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts @@ -6,8 +6,10 @@ import { AbstractVectorSource } from '../vector_source'; import { IVectorSource } from '../vector_source'; -import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/public'; +import { IndexPattern, ISearchSource } from '../../../../../../../src/plugins/data/public'; import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; export interface IESSource extends IVectorSource { getId(): string; @@ -19,7 +21,14 @@ export interface IESSource extends IVectorSource { searchFilters: VectorSourceRequestMeta, limit: number, initialSearchContext?: object - ): Promise; + ): Promise; + loadStylePropsMeta( + layerName: string, + style: VectorStyle, + dynamicStyleProps: IDynamicStyleProperty[], + registerCancelCallback: (requestToken: symbol, callback: () => void) => void, + searchFilters: VectorSourceRequestMeta + ): Promise; } export class AbstractESSource extends AbstractVectorSource implements IESSource { @@ -32,5 +41,12 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource searchFilters: VectorSourceRequestMeta, limit: number, initialSearchContext?: object - ): Promise; + ): Promise; + loadStylePropsMeta( + layerName: string, + style: VectorStyle, + dynamicStyleProps: IDynamicStyleProperty[], + registerCancelCallback: (requestToken: symbol, callback: () => void) => void, + searchFilters: VectorSourceRequestMeta + ): Promise; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js index 3402e367cbd73..ccd8bc4a859db 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js @@ -9,14 +9,14 @@ import { getAutocompleteService, fetchSearchSourceAndRecordWithInspector, getIndexPatternService, - SearchSource, getTimeFilter, + getSearchService, } from '../../../kibana_services'; import { createExtentFilter } from '../../../elasticsearch_geo_utils'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths + import { copyPersistentState } from '../../../reducers/util'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { DataRequestAbortError } from '../../util/data_request'; @@ -125,8 +125,9 @@ export class AbstractESSource extends AbstractVectorSource { if (isTimeAware) { allFilters.push(getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters)); } + const searchService = getSearchService(); + const searchSource = searchService.searchSource.create(initialSearchContext); - const searchSource = new SearchSource(initialSearchContext); searchSource.setField('index', indexPattern); searchSource.setField('size', limit); searchSource.setField('filter', allFilters); @@ -135,7 +136,8 @@ export class AbstractESSource extends AbstractVectorSource { } if (searchFilters.sourceQuery) { - const layerSearchSource = new SearchSource(); + const layerSearchSource = searchService.searchSource.create(); + layerSearchSource.setField('index', indexPattern); layerSearchSource.setField('query', searchFilters.sourceQuery); searchSource.setParent(layerSearchSource); @@ -294,7 +296,9 @@ export class AbstractESSource extends AbstractVectorSource { }, {}); const indexPattern = await this.getIndexPattern(); - const searchSource = new SearchSource(); + const searchService = getSearchService(); + const searchSource = searchService.searchSource.create(); + searchSource.setField('index', indexPattern); searchSource.setField('size', 0); searchSource.setField('aggs', aggs); diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts index 701bd5e2c8b5e..248ca2b9212b4 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts @@ -9,4 +9,5 @@ import { IESAggSource } from '../es_agg_source'; export interface IESTermSource extends IESAggSource { getTermField(): IField; + hasCompleteConfig(): boolean; } diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 0bfda6be72203..a73cfbdc0d043 100644 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -15,9 +15,9 @@ import { IField } from '../../fields/field'; import { registerSource } from '../source_registry'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { - LayerDescriptor, MapExtent, TiledSingleLayerVectorSourceDescriptor, + VectorLayerDescriptor, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; @@ -66,12 +66,15 @@ export class MVTSingleLayerVectorSource extends AbstractSource return []; } - createDefaultLayer(options: LayerDescriptor): TiledVectorLayer { - const layerDescriptor = { + createDefaultLayer(options?: Partial): TiledVectorLayer { + const layerDescriptor: Partial = { sourceDescriptor: this._descriptor, ...options, }; - const normalizedLayerDescriptor = TiledVectorLayer.createDescriptor(layerDescriptor, []); + const normalizedLayerDescriptor: VectorLayerDescriptor = TiledVectorLayer.createDescriptor( + layerDescriptor, + [] + ); const vectorLayerArguments: VectorLayerArguments = { layerDescriptor: normalizedLayerDescriptor, source: this, diff --git a/x-pack/plugins/maps/public/layers/sources/source.d.ts b/x-pack/plugins/maps/public/layers/sources/source.d.ts deleted file mode 100644 index 5a01da02adaae..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/source.d.ts +++ /dev/null @@ -1,56 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -import { AbstractSourceDescriptor, LayerDescriptor } from '../../../common/descriptor_types'; -import { ILayer } from '../layer'; - -export type ImmutableSourceProperty = { - label: string; - value: string; -}; - -export type Attribution = { - url: string; - label: string; -}; - -export interface ISource { - createDefaultLayer(options?: LayerDescriptor): ILayer; - destroy(): void; - getDisplayName(): Promise; - getInspectorAdapters(): object; - isFieldAware(): boolean; - isFilterByMapBounds(): boolean; - isGeoGridPrecisionAware(): boolean; - isQueryAware(): boolean; - isRefreshTimerAware(): Promise; - isTimeAware(): Promise; - getImmutableProperties(): Promise; - getAttributions(): Promise; - getMinZoom(): number; - getMaxZoom(): number; -} - -export class AbstractSource implements ISource { - readonly _descriptor: AbstractSourceDescriptor; - constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters?: object); - - destroy(): void; - createDefaultLayer(options?: LayerDescriptor, mapColors?: string[]): ILayer; - getDisplayName(): Promise; - getInspectorAdapters(): object; - isFieldAware(): boolean; - isFilterByMapBounds(): boolean; - isGeoGridPrecisionAware(): boolean; - isQueryAware(): boolean; - isRefreshTimerAware(): Promise; - isTimeAware(): Promise; - getImmutableProperties(): Promise; - getAttributions(): Promise; - getMinZoom(): number; - getMaxZoom(): number; -} diff --git a/x-pack/plugins/maps/public/layers/sources/source.js b/x-pack/plugins/maps/public/layers/sources/source.js deleted file mode 100644 index 555b8999d6284..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/source.js +++ /dev/null @@ -1,160 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../../reducers/util'; -import { MIN_ZOOM, MAX_ZOOM } from '../../../common/constants'; - -export class AbstractSource { - static isIndexingSource = false; - - static renderEditor() { - throw new Error('Must implement Source.renderEditor'); - } - - static createDescriptor() { - throw new Error('Must implement Source.createDescriptor'); - } - - constructor(descriptor, inspectorAdapters) { - this._descriptor = descriptor; - this._inspectorAdapters = inspectorAdapters; - } - - destroy() {} - - cloneDescriptor() { - return copyPersistentState(this._descriptor); - } - - async supportsFitToBounds() { - return true; - } - - /** - * return list of immutable source properties. - * Immutable source properties are properties that can not be edited by the user. - */ - async getImmutableProperties() { - return []; - } - - getInspectorAdapters() { - return this._inspectorAdapters; - } - - _createDefaultLayerDescriptor() { - throw new Error(`Source#createDefaultLayerDescriptor not implemented`); - } - - createDefaultLayer() { - throw new Error(`Source#createDefaultLayer not implemented`); - } - - async getDisplayName() { - console.warn('Source should implement Source#getDisplayName'); - return ''; - } - - /** - * return attribution for this layer as array of objects with url and label property. - * e.g. [{ url: 'example.com', label: 'foobar' }] - * @return {Promise} - */ - async getAttributions() { - return []; - } - - isFieldAware() { - return false; - } - - isRefreshTimerAware() { - return false; - } - - isGeoGridPrecisionAware() { - return false; - } - - async isTimeAware() { - return false; - } - - getFieldNames() { - return []; - } - - hasCompleteConfig() { - throw new Error(`Source#hasCompleteConfig not implemented`); - } - - renderSourceSettingsEditor() { - return null; - } - - getApplyGlobalQuery() { - return !!this._descriptor.applyGlobalQuery; - } - - getIndexPatternIds() { - return []; - } - - getQueryableIndexPatternIds() { - return []; - } - - isFilterByMapBounds() { - return false; - } - - isQueryAware() { - return false; - } - - getGeoGridPrecision() { - return 0; - } - - isJoinable() { - return false; - } - - shouldBeIndexed() { - return AbstractSource.isIndexingSource; - } - - isESSource() { - return false; - } - - // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes - async getPreIndexedShape(/* properties */) { - return null; - } - - // Returns function used to format value - async createFieldFormatter(/* field */) { - return null; - } - - async loadStylePropsMeta() { - throw new Error(`Source#loadStylePropsMeta not implemented`); - } - - async getValueSuggestions(/* field, query */) { - return []; - } - - getMinZoom() { - return MIN_ZOOM; - } - - getMaxZoom() { - return MAX_ZOOM; - } -} diff --git a/x-pack/plugins/maps/public/layers/sources/source.ts b/x-pack/plugins/maps/public/layers/sources/source.ts new file mode 100644 index 0000000000000..1cd84010159ab --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/source.ts @@ -0,0 +1,195 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { ReactElement } from 'react'; + +import { Adapters } from 'src/plugins/inspector/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +// @ts-ignore +import { copyPersistentState } from '../../reducers/util'; + +import { LayerDescriptor, SourceDescriptor } from '../../../common/descriptor_types'; +import { ILayer } from '../layer'; +import { IField } from '../fields/field'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; + +export type ImmutableSourceProperty = { + label: string; + value: string; +}; + +export type Attribution = { + url: string; + label: string; +}; + +export type PreIndexedShape = { + index: string; + id: string | number; + path: string; +}; + +export type FieldFormatter = (value: string | number | null | undefined | boolean) => string; + +export interface ISource { + createDefaultLayer(options?: Partial): ILayer; + destroy(): void; + getDisplayName(): Promise; + getInspectorAdapters(): Adapters | undefined; + isFieldAware(): boolean; + isFilterByMapBounds(): boolean; + isGeoGridPrecisionAware(): boolean; + isQueryAware(): boolean; + isRefreshTimerAware(): boolean; + isTimeAware(): Promise; + getImmutableProperties(): Promise; + getAttributions(): Promise; + isESSource(): boolean; + renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null; + supportsFitToBounds(): Promise; + isJoinable(): boolean; + cloneDescriptor(): SourceDescriptor; + getFieldNames(): string[]; + getApplyGlobalQuery(): boolean; + getIndexPatternIds(): string[]; + getQueryableIndexPatternIds(): string[]; + getGeoGridPrecision(zoom: number): number; + shouldBeIndexed(): boolean; + getPreIndexedShape(): Promise; + createFieldFormatter(field: IField): Promise; + getValueSuggestions(field: IField, query: string): Promise; + getMinZoom(): number; + getMaxZoom(): number; +} + +export class AbstractSource implements ISource { + readonly _descriptor: SourceDescriptor; + readonly _inspectorAdapters?: Adapters | undefined; + + constructor(descriptor: SourceDescriptor, inspectorAdapters?: Adapters) { + this._descriptor = descriptor; + this._inspectorAdapters = inspectorAdapters; + } + + destroy(): void {} + + cloneDescriptor(): SourceDescriptor { + // @ts-ignore + return copyPersistentState(this._descriptor); + } + + async supportsFitToBounds(): Promise { + return true; + } + + /** + * return list of immutable source properties. + * Immutable source properties are properties that can not be edited by the user. + */ + async getImmutableProperties(): Promise { + return []; + } + + getInspectorAdapters(): Adapters | undefined { + return this._inspectorAdapters; + } + + createDefaultLayer(options?: Partial): ILayer { + throw new Error(`Source#createDefaultLayer not implemented`); + } + + async getDisplayName(): Promise { + return ''; + } + + async getAttributions(): Promise { + return []; + } + + isFieldAware(): boolean { + return false; + } + + isRefreshTimerAware(): boolean { + return false; + } + + isGeoGridPrecisionAware(): boolean { + return false; + } + + isQueryAware(): boolean { + return false; + } + + getFieldNames(): string[] { + return []; + } + + renderSourceSettingsEditor() { + return null; + } + + getApplyGlobalQuery(): boolean { + return !!this._descriptor.applyGlobalQuery; + } + + getIndexPatternIds(): string[] { + return []; + } + + getQueryableIndexPatternIds(): string[] { + return []; + } + + getGeoGridPrecision(zoom: number): number { + return 0; + } + + isJoinable(): boolean { + return false; + } + + shouldBeIndexed(): boolean { + return false; + } + + isESSource(): boolean { + return false; + } + + // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes + async getPreIndexedShape(/* properties */): Promise { + return null; + } + + // Returns function used to format value + async createFieldFormatter(field: IField): Promise { + return null; + } + + async getValueSuggestions(field: IField, query: string): Promise { + return []; + } + + async isTimeAware(): Promise { + return false; + } + + isFilterByMapBounds(): boolean { + return false; + } + + getMinZoom() { + return MIN_ZOOM; + } + + getMaxZoom() { + return MAX_ZOOM; + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts index 8b64480f92961..77f8d88a8c0ab 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts @@ -13,6 +13,7 @@ import { AbstractTMSSource } from '../tms_source'; import { LayerDescriptor, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; import { Attribution, ImmutableSourceProperty } from '../source'; import { XYZTMSSourceConfig } from './xyz_tms_editor'; +import { ILayer } from '../../layer'; export const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', { defaultMessage: 'Tile Map Service', @@ -48,7 +49,7 @@ export class XYZTMSSource extends AbstractTMSSource { ]; } - createDefaultLayer(options?: LayerDescriptor): TileLayer { + createDefaultLayer(options?: LayerDescriptor): ILayer { const layerDescriptor: LayerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: this._descriptor, ...options, diff --git a/x-pack/plugins/maps/public/layers/styles/abstract_style.js b/x-pack/plugins/maps/public/layers/styles/abstract_style.js deleted file mode 100644 index 3e7a3dbf7ed20..0000000000000 --- a/x-pack/plugins/maps/public/layers/styles/abstract_style.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export class AbstractStyle { - getDescriptorWithMissingStylePropsRemoved(/* nextOrdinalFields */) { - return { - hasChanges: false, - }; - } - - async pluckStyleMetaFromSourceDataRequest(/* sourceDataRequest */) { - return {}; - } - - getDescriptor() { - return this._descriptor; - } - - renderEditor(/* { layer, onStyleDescriptorChange } */) { - return null; - } - - getSourceFieldNames() { - return []; - } -} diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index d769fe0da9ec2..1fa24943c5e51 100644 --- a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { AbstractStyle } from '../abstract_style'; +import { AbstractStyle } from '../style'; import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; diff --git a/x-pack/plugins/maps/public/layers/styles/style.ts b/x-pack/plugins/maps/public/layers/styles/style.ts new file mode 100644 index 0000000000000..38fdc36904412 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/styles/style.ts @@ -0,0 +1,59 @@ +/* + * 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 { ReactElement } from 'react'; +import { StyleDescriptor, StyleMetaDescriptor } from '../../../common/descriptor_types'; +import { ILayer } from '../layer'; +import { IField } from '../fields/field'; +import { DataRequest } from '../util/data_request'; + +export interface IStyle { + getDescriptor(): StyleDescriptor | null; + getDescriptorWithMissingStylePropsRemoved( + nextFields: IField[] + ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor }; + pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor; + renderEditor({ + layer, + onStyleDescriptorChange, + }: { + layer: ILayer; + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement | null; + getSourceFieldNames(): string[]; +} + +export class AbstractStyle implements IStyle { + readonly _descriptor: StyleDescriptor | null; + + constructor(descriptor: StyleDescriptor | null) { + this._descriptor = descriptor; + } + + getDescriptorWithMissingStylePropsRemoved( + nextFields: IField[] + ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor } { + return { + hasChanges: false, + }; + } + + pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor { + return { fieldMeta: {} }; + } + + getDescriptor(): StyleDescriptor | null { + return this._descriptor; + } + + renderEditor(/* { layer, onStyleDescriptorChange } */) { + return null; + } + + getSourceFieldNames(): string[] { + return []; + } +} diff --git a/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts new file mode 100644 index 0000000000000..f658d0821edf2 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts @@ -0,0 +1,16 @@ +/* + * 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 { AbstractStyle } from '../style'; +import { LAYER_STYLE_TYPE } from '../../../../common/constants'; + +export class TileStyle extends AbstractStyle { + constructor() { + super({ + type: LAYER_STYLE_TYPE.TILE, + }); + } +} diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts index e010d5ac7d7a3..762322b8e09f9 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts @@ -7,24 +7,23 @@ import { IStyleProperty } from './properties/style_property'; import { IDynamicStyleProperty } from './properties/dynamic_style_property'; import { IVectorLayer } from '../../vector_layer'; import { IVectorSource } from '../../sources/vector_source'; +import { AbstractStyle, IStyle } from '../style'; import { VectorStyleDescriptor, VectorStylePropertiesDescriptor, } from '../../../../common/descriptor_types'; -export interface IVectorStyle { +export interface IVectorStyle extends IStyle { getAllStyleProperties(): IStyleProperty[]; - getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; getSourceFieldNames(): string[]; } -export class VectorStyle implements IVectorStyle { +export class VectorStyle extends AbstractStyle implements IVectorStyle { static createDescriptor(properties: VectorStylePropertiesDescriptor): VectorStyleDescriptor; static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor; constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer); getSourceFieldNames(): string[]; getAllStyleProperties(): IStyleProperty[]; - getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; } diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js index b044c98d44d41..5a4edd9c93a05 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { VectorStyleEditor } from './components/vector_style_editor'; import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES } from './vector_style_defaults'; -import { AbstractStyle } from '../abstract_style'; +import { AbstractStyle } from '../style'; import { GEO_JSON_TYPE, FIELD_ORIGIN, @@ -60,6 +60,7 @@ export class VectorStyle extends AbstractStyle { constructor(descriptor = {}, source, layer) { super(); + descriptor = descriptor === null ? {} : descriptor; this._source = source; this._layer = layer; this._descriptor = { diff --git a/x-pack/plugins/maps/public/layers/tile_layer.d.ts b/x-pack/plugins/maps/public/layers/tile_layer.d.ts index 53e8c388ee4c2..8a1ef0f172717 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.d.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractLayer, ILayerArguments } from './layer'; +import { AbstractLayer } from './layer'; import { ITMSSource } from './sources/tms_source'; import { LayerDescriptor } from '../../common/descriptor_types'; -interface ITileLayerArguments extends ILayerArguments { +interface ITileLayerArguments { source: ITMSSource; layerDescriptor: LayerDescriptor; } diff --git a/x-pack/plugins/maps/public/layers/tile_layer.js b/x-pack/plugins/maps/public/layers/tile_layer.js index 2ac60e12d137a..baded3c287637 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/plugins/maps/public/layers/tile_layer.js @@ -6,7 +6,8 @@ import { AbstractLayer } from './layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; +import { TileStyle } from './styles/tile/tile_style'; export class TileLayer extends AbstractLayer { static type = LAYER_TYPE.TILE; @@ -15,9 +16,14 @@ export class TileLayer extends AbstractLayer { const tileLayerDescriptor = super.createDescriptor(options, mapColors); tileLayerDescriptor.type = TileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } + constructor({ source, layerDescriptor }) { + super({ source, layerDescriptor, style: new TileStyle() }); + } + async syncData({ startLoading, stopLoading, onLoadError, dataFilters }) { if (!this.isVisible() || !this.showAtZoomLevel(dataFilters.zoom)) { return; diff --git a/x-pack/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/plugins/maps/public/layers/tile_layer.test.ts index f8c2fd9db60fa..a7e8be9fc4b46 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TileLayer } from './tile_layer'; +// eslint-disable-next-line max-classes-per-file +import { ITileLayerArguments, TileLayer } from './tile_layer'; import { SOURCE_TYPES } from '../../common/constants'; import { XYZTMSSourceDescriptor } from '../../common/descriptor_types'; import { ITMSSource, AbstractTMSSource } from './sources/tms_source'; @@ -38,10 +39,13 @@ class MockTileSource extends AbstractTMSSource implements ITMSSource { describe('TileLayer', () => { it('should use display-label from source', async () => { const source = new MockTileSource(sourceDescriptor); - const layer: ILayer = new TileLayer({ + + const args: ITileLayerArguments = { source, layerDescriptor: { id: 'layerid', sourceDescriptor }, - }); + }; + + const layer: ILayer = new TileLayer(args); expect(await source.getDisplayName()).toEqual(await layer.getDisplayName()); }); diff --git a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx index c47cae5641e56..06c5ef579b221 100644 --- a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx @@ -20,7 +20,7 @@ export class TiledVectorLayer extends VectorLayer { static type = LAYER_TYPE.TILED_VECTOR; static createDescriptor( - descriptor: VectorLayerDescriptor, + descriptor: Partial, mapColors: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); diff --git a/x-pack/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/plugins/maps/public/layers/vector_layer.d.ts index 3d5b8054ff3fd..efc1f3011c687 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/vector_layer.d.ts @@ -19,7 +19,7 @@ import { IVectorStyle } from './styles/vector/vector_style'; import { IField } from './fields/field'; import { SyncContext } from '../actions/map_actions'; -type VectorLayerArguments = { +export type VectorLayerArguments = { source: IVectorSource; joins?: IJoin[]; layerDescriptor: VectorLayerDescriptor; @@ -33,14 +33,12 @@ export interface IVectorLayer extends ILayer { } export class VectorLayer extends AbstractLayer implements IVectorLayer { + protected readonly _style: IVectorStyle; static createDescriptor( options: Partial, mapColors?: string[] ): VectorLayerDescriptor; - protected readonly _source: IVectorSource; - protected readonly _style: IVectorStyle; - constructor(options: VectorLayerArguments); getLayerTypeIconName(): string; getFields(): Promise; diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/layers/vector_layer.js index c5947a63587ea..17b7f8152d76d 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_layer.js @@ -484,6 +484,8 @@ export class VectorLayer extends AbstractLayer { try { startLoading(dataRequestId, requestToken, nextMeta); const layerName = await this.getDisplayName(source); + + //todo: cast source to ESSource when migrating to TS const styleMeta = await source.loadStylePropsMeta( layerName, style, diff --git a/x-pack/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/plugins/maps/public/layers/vector_tile_layer.js index c620ec6c56dc3..fc7812a2c86c7 100644 --- a/x-pack/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_tile_layer.js @@ -6,7 +6,7 @@ import { TileLayer } from './tile_layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; import { isRetina } from '../meta'; import { addSpriteSheetToMapFromImageData, @@ -28,6 +28,7 @@ export class VectorTileLayer extends TileLayer { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = VectorTileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.js b/x-pack/plugins/maps/public/maps_vis_type_alias.js new file mode 100644 index 0000000000000..85613f4608c6f --- /dev/null +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.js @@ -0,0 +1,41 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { APP_ID, APP_ICON, MAP_BASE_URL } from '../common/constants'; +import { getInjectedVarFunc, getVisualizations } from './kibana_services'; + +export function getMapsVisTypeAlias() { + const showMapVisualizationTypes = getInjectedVarFunc()('showMapVisualizationTypes', false); + if (!showMapVisualizationTypes) { + getVisualizations().hideTypes(['region_map', 'tile_map']); + } + + const description = i18n.translate('xpack.maps.visTypeAlias.description', { + defaultMessage: 'Create and style maps with multiple layers and indices.', + }); + + const legacyMapVisualizationWarning = i18n.translate( + 'xpack.maps.visTypeAlias.legacyMapVizWarning', + { + defaultMessage: `Use the Maps app instead of Coordinate Map and Region Map. +The Maps app offers more functionality and is easier to use.`, + } + ); + + return { + aliasUrl: MAP_BASE_URL, + name: APP_ID, + title: i18n.translate('xpack.maps.visTypeAlias.title', { + defaultMessage: 'Maps', + }), + description: showMapVisualizationTypes + ? `${description} ${legacyMapVisualizationWarning}` + : description, + icon: APP_ICON, + stage: 'production', + }; +} diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index d3b9626dc8366..bdcd14ea98782 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -31,12 +31,23 @@ import { setUiActions, setUiSettings, setVisualizations, - // @ts-ignore + setSearchService, } from './kibana_services'; +import { featureCatalogueEntry } from './feature_catalogue_entry'; +// @ts-ignore +import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import { registerLayerWizards } from './layers/load_layer_wizards'; +import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { MapEmbeddableFactory } from './embeddable'; +import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; + home: HomePublicPluginSetup; + visualizations: VisualizationsSetup; + embeddable: EmbeddableSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsPluginStartDependencies {} @@ -61,6 +72,7 @@ export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { setFileUpload(fileUpload); setIndexPatternSelect(data.ui.IndexPatternSelect); setTimeFilter(data.query.timefilter.timefilter); + setSearchService(data.search); setIndexPatternService(data.indexPatterns); setAutocompleteService(data.autocomplete); setCore(core); @@ -94,8 +106,16 @@ export class MapsPlugin MapsPluginStartDependencies > { public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { - plugins.inspector.registerView(MapView); + const { inspector, home, visualizations, embeddable } = plugins; + bindSetupCoreAndPlugins(core, plugins); + + inspector.registerView(MapView); + home.featureCatalogue.register(featureCatalogueEntry); + visualizations.registerAlias(getMapsVisTypeAlias()); + embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); } - public start(core: CoreStart, plugins: any) {} + public start(core: CoreStart, plugins: any) { + bindStartCoreAndPlugins(core, plugins); + } } diff --git a/x-pack/plugins/maps/public/reducers/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts new file mode 100644 index 0000000000000..fe21b37434edd --- /dev/null +++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts @@ -0,0 +1,19 @@ +/* + * 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 { MAX_ZOOM, MIN_ZOOM } from '../../common/constants'; +import { MapSettings } from './map'; + +export function getDefaultMapSettings(): MapSettings { + return { + maxZoom: MAX_ZOOM, + minZoom: MIN_ZOOM, + showSpatialFilters: true, + spatialFiltersAlpa: 0.3, + spatialFiltersFillColor: '#DA8B45', + spatialFiltersLineColor: '#DA8B45', + }; +} diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 30271d4d5fa8b..be0700d4bdd6d 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -39,6 +39,15 @@ export type MapContext = { hideViewControl: boolean; }; +export type MapSettings = { + maxZoom: number; + minZoom: number; + showSpatialFilters: boolean; + spatialFiltersAlpa: number; + spatialFiltersFillColor: string; + spatialFiltersLineColor: string; +}; + export type MapState = { ready: boolean; mapInitError?: string | null; @@ -49,4 +58,6 @@ export type MapState = { __transientLayerId: string | null; layerList: LayerDescriptor[]; waitingForMapReadyLayerList: LayerDescriptor[]; + settings: MapSettings; + __rollbackSettings: MapSettings | null; }; diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 251a2304538ed..a76267bb7095e 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -46,8 +46,13 @@ import { HIDE_LAYER_CONTROL, HIDE_VIEW_CONTROL, SET_WAITING_FOR_READY_HIDDEN_LAYERS, + SET_MAP_SETTINGS, + ROLLBACK_MAP_SETTINGS, + TRACK_MAP_SETTINGS, + UPDATE_MAP_SETTING, } from '../actions/map_actions'; +import { getDefaultMapSettings } from './default_map_settings'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util'; import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; @@ -124,6 +129,8 @@ const INITIAL_STATE = { __transientLayerId: null, layerList: [], waitingForMapReadyLayerList: [], + settings: getDefaultMapSettings(), + __rollbackSettings: null, }; export function map(state = INITIAL_STATE, action) { @@ -179,6 +186,32 @@ export function map(state = INITIAL_STATE, action) { ...state, goto: null, }; + case SET_MAP_SETTINGS: + return { + ...state, + settings: { ...getDefaultMapSettings(), ...action.settings }, + }; + case ROLLBACK_MAP_SETTINGS: + return state.__rollbackSettings + ? { + ...state, + settings: { ...state.__rollbackSettings }, + __rollbackSettings: null, + } + : state; + case TRACK_MAP_SETTINGS: + return { + ...state, + __rollbackSettings: state.settings, + }; + case UPDATE_MAP_SETTING: + return { + ...state, + settings: { + ...(state.settings ? state.settings : {}), + [action.settingKey]: action.settingValue, + }, + }; case SET_LAYER_ERROR_STATUS: const { layerList } = state; const layerIdx = getLayerIndex(layerList, action.layerId); diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index 7429545ec0e46..537ea7fc7b24b 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -16,13 +16,13 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, UPDATE_INDEXING_STAGE, - // @ts-ignore } from '../actions/ui_actions'; export enum FLYOUT_STATE { NONE = 'NONE', LAYER_PANEL = 'LAYER_PANEL', ADD_LAYER_WIZARD = 'ADD_LAYER_WIZARD', + MAP_SETTINGS_PANEL = 'MAP_SETTINGS_PANEL', } export enum INDEXING_STAGE { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts new file mode 100644 index 0000000000000..4d0f652af982a --- /dev/null +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.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 { AnyAction } from 'redux'; +import { MapCenter } from '../../common/descriptor_types'; +import { MapStoreState } from '../reducers/store'; +import { MapSettings } from '../reducers/map'; +import { IVectorLayer } from '../layers/vector_layer'; + +export function getHiddenLayerIds(state: MapStoreState): string[]; + +export function getMapZoom(state: MapStoreState): number; + +export function getMapCenter(state: MapStoreState): MapCenter; + +export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[]; + +export function getMapSettings(state: MapStoreState): MapSettings; + +export function hasMapSettingsChanges(state: MapStoreState): boolean; + +export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer; diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js similarity index 76% rename from x-pack/legacy/plugins/maps/public/selectors/map_selectors.js rename to x-pack/plugins/maps/public/selectors/map_selectors.js index 1e71025935519..f43c92d4c9945 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -6,31 +6,26 @@ import { createSelector } from 'reselect'; import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TileLayer } from '../../../../../plugins/maps/public/layers/tile_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { VectorTileLayer } from '../../../../../plugins/maps/public/layers/vector_tile_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { VectorLayer } from '../../../../../plugins/maps/public/layers/vector_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { HeatmapLayer } from '../../../../../plugins/maps/public/layers/heatmap_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BlendedVectorLayer } from '../../../../../plugins/maps/public/layers/blended_vector_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getTimeFilter } from '../../../../../plugins/maps/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TiledVectorLayer } from '../../../../../plugins/maps/public/layers/tiled_vector_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; +import { TileLayer } from '../layers/tile_layer'; +import { VectorTileLayer } from '../layers/vector_tile_layer'; +import { VectorLayer } from '../layers/vector_layer'; +import { HeatmapLayer } from '../layers/heatmap_layer'; +import { BlendedVectorLayer } from '../layers/blended_vector_layer'; +import { getTimeFilter } from '../kibana_services'; +import { getInspectorAdapters } from '../reducers/non_serializable_instances'; +import { TiledVectorLayer } from '../layers/tiled_vector_layer'; +import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; +import { InnerJoin } from '../layers/joins/inner_join'; +import { getSourceByType } from '../layers/sources/source_registry'; +import { GeojsonFileSource } from '../layers/sources/client_file_source'; import { - copyPersistentState, - TRACKED_LAYER_DESCRIPTOR, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/util'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { InnerJoin } from '../../../../../plugins/maps/public/layers/joins/inner_join'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getSourceByType } from '../../../../../plugins/maps/public/layers/sources/source_registry'; + LAYER_TYPE, + SOURCE_DATA_ID_ORIGIN, + STYLE_TYPE, + VECTOR_STYLES, + SPATIAL_FILTERS_LAYER_ID, +} from '../../common/constants'; +import { extractFeaturesFromFilters } from '../elasticsearch_geo_utils'; function createLayerInstance(layerDescriptor, inspectorAdapters) { const source = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); @@ -68,6 +63,18 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new source.ConstructorFunction(sourceDescriptor, inspectorAdapters); } +export const getMapSettings = ({ map }) => map.settings; + +const getRollbackMapSettings = ({ map }) => map.__rollbackSettings; + +export const hasMapSettingsChanges = createSelector( + getMapSettings, + getRollbackMapSettings, + (settings, rollbackSettings) => { + return rollbackSettings ? !_.isEqual(settings, rollbackSettings) : false; + } +); + export const getOpenTooltips = ({ map }) => { return map && map.openTooltips ? map.openTooltips : []; }; @@ -187,6 +194,53 @@ export const getDataFilters = createSelector( } ); +export const getSpatialFiltersLayer = createSelector( + getFilters, + getMapSettings, + (filters, settings) => { + const featureCollection = { + type: 'FeatureCollection', + features: extractFeaturesFromFilters(filters), + }; + const geoJsonSourceDescriptor = GeojsonFileSource.createDescriptor( + featureCollection, + 'spatialFilters' + ); + + return new VectorLayer({ + layerDescriptor: { + id: SPATIAL_FILTERS_LAYER_ID, + visible: settings.showSpatialFilters, + alpha: settings.spatialFiltersAlpa, + type: LAYER_TYPE.VECTOR, + __dataRequests: [ + { + dataId: SOURCE_DATA_ID_ORIGIN, + data: featureCollection, + }, + ], + style: { + properties: { + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: settings.spatialFiltersFillColor, + }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: settings.spatialFiltersLineColor, + }, + }, + }, + }, + }, + source: new GeojsonFileSource(geoJsonSourceDescriptor), + }); + } +); + export const getLayerList = createSelector( getLayerListRaw, getInspectorAdapters, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/plugins/maps/public/selectors/map_selectors.test.js new file mode 100644 index 0000000000000..fec16251914ea --- /dev/null +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.js @@ -0,0 +1,56 @@ +/* + * 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. + */ + +jest.mock('../layers/vector_layer', () => {}); +jest.mock('../layers/tiled_vector_layer', () => {}); +jest.mock('../layers/blended_vector_layer', () => {}); +jest.mock('../layers/heatmap_layer', () => {}); +jest.mock('../layers/vector_tile_layer', () => {}); +jest.mock('../layers/joins/inner_join', () => {}); +jest.mock('../reducers/non_serializable_instances', () => ({ + getInspectorAdapters: () => { + return {}; + }, +})); +jest.mock('../kibana_services', () => ({ + getTimeFilter: () => ({ + getTime: () => { + return { + to: 'now', + from: 'now-15m', + }; + }, + }), +})); + +import { getTimeFilters } from './map_selectors'; + +describe('getTimeFilters', () => { + it('should return timeFilters when contained in state', () => { + const state = { + map: { + mapState: { + timeFilters: { + to: '2001-01-01', + from: '2001-12-31', + }, + }, + }, + }; + expect(getTimeFilters(state)).toEqual({ to: '2001-01-01', from: '2001-12-31' }); + }); + + it('should return kibana time filters when not contained in state', () => { + const state = { + map: { + mapState: { + timeFilters: null, + }, + }, + }; + expect(getTimeFilters(state)).toEqual({ to: 'now', from: 'now-15m' }); + }); +}); diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts new file mode 100644 index 0000000000000..32d4beeb381d7 --- /dev/null +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MapStoreState } from '../reducers/store'; + +import { FLYOUT_STATE, INDEXING_STAGE } from '../reducers/ui'; + +export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; +export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; +export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; +export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; +export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; +export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getIndexingStage = ({ ui }: MapStoreState): INDEXING_STAGE | null => + ui.importIndexingStage; diff --git a/x-pack/plugins/ml/common/license/index.ts b/x-pack/plugins/ml/common/license/index.ts index e901a9545897b..e87986a26a3bd 100644 --- a/x-pack/plugins/ml/common/license/index.ts +++ b/x-pack/plugins/ml/common/license/index.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MlLicense, LicenseStatus, MINIMUM_FULL_LICENSE, MINIMUM_LICENSE } from './ml_license'; +export { + MlLicense, + LicenseStatus, + MINIMUM_FULL_LICENSE, + MINIMUM_LICENSE, + isFullLicense, + isMinimumLicense, +} from './ml_license'; diff --git a/x-pack/plugins/ml/common/license/ml_license.ts b/x-pack/plugins/ml/common/license/ml_license.ts index 2a60887310447..25b5b4992b227 100644 --- a/x-pack/plugins/ml/common/license/ml_license.ts +++ b/x-pack/plugins/ml/common/license/ml_license.ts @@ -38,8 +38,8 @@ export class MlLicense { this._isSecurityEnabled = securityIsEnabled; this._hasLicenseExpired = this._license.status === 'expired'; this._isMlEnabled = this._license.getFeature(PLUGIN_ID).isEnabled; - this._isMinimumLicense = this._license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; - this._isFullLicense = this._license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; + this._isMinimumLicense = isMinimumLicense(this._license); + this._isFullLicense = isFullLicense(this._license); if (this._initialized === false && postInitFunctions !== undefined) { postInitFunctions.forEach(f => f(this)); @@ -74,3 +74,11 @@ export class MlLicense { return this._isFullLicense; } } + +export function isFullLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; +} + +export function isMinimumLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; +} diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts new file mode 100644 index 0000000000000..2a449c95faa5b --- /dev/null +++ b/x-pack/plugins/ml/common/types/capabilities.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 { KibanaRequest } from 'kibana/server'; + +export const userMlCapabilities = { + // Anomaly Detection + canGetJobs: false, + canGetDatafeeds: false, + // Calendars + canGetCalendars: false, + // File Data Visualizer + canFindFileStructure: false, + // Filters + canGetFilters: false, + // Data Frame Analytics + canGetDataFrameAnalytics: false, +}; + +export const adminMlCapabilities = { + // Anomaly Detection + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canForecastJob: false, + canStartStopDatafeed: false, + canUpdateJob: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + // Calendars + canCreateCalendar: false, + canDeleteCalendar: false, + // Filters + canCreateFilter: false, + canDeleteFilter: false, + // Data Frame Analytics + canDeleteDataFrameAnalytics: false, + canCreateDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, +}; + +export type UserMlCapabilities = typeof userMlCapabilities; +export type AdminMlCapabilities = typeof adminMlCapabilities; +export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; + +export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array; + +export function getDefaultCapabilities(): MlCapabilities { + return { + ...userMlCapabilities, + ...adminMlCapabilities, + }; +} + +export interface MlCapabilitiesResponse { + capabilities: MlCapabilities; + upgradeInProgress: boolean; + isPlatinumOrTrialLicense: boolean; + mlFeatureEnabledInSpace: boolean; +} + +export type ResolveMlCapabilities = (request: KibanaRequest) => Promise; diff --git a/x-pack/plugins/ml/common/types/privileges.ts b/x-pack/plugins/ml/common/types/privileges.ts deleted file mode 100644 index d9089c751b81b..0000000000000 --- a/x-pack/plugins/ml/common/types/privileges.ts +++ /dev/null @@ -1,75 +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. - */ -// - -export interface Privileges { - // Anomaly Detection - canGetJobs: boolean; - canCreateJob: boolean; - canDeleteJob: boolean; - canOpenJob: boolean; - canCloseJob: boolean; - canForecastJob: boolean; - canGetDatafeeds: boolean; - canStartStopDatafeed: boolean; - canUpdateJob: boolean; - canUpdateDatafeed: boolean; - canPreviewDatafeed: boolean; - // Calendars - canGetCalendars: boolean; - canCreateCalendar: boolean; - canDeleteCalendar: boolean; - // Filters - canGetFilters: boolean; - canCreateFilter: boolean; - canDeleteFilter: boolean; - // File Data Visualizer - canFindFileStructure: boolean; - // Data Frame Analytics - canGetDataFrameAnalytics: boolean; - canDeleteDataFrameAnalytics: boolean; - canCreateDataFrameAnalytics: boolean; - canStartStopDataFrameAnalytics: boolean; -} - -export function getDefaultPrivileges(): Privileges { - return { - // Anomaly Detection - canGetJobs: false, - canCreateJob: false, - canDeleteJob: false, - canOpenJob: false, - canCloseJob: false, - canForecastJob: false, - canGetDatafeeds: false, - canStartStopDatafeed: false, - canUpdateJob: false, - canUpdateDatafeed: false, - canPreviewDatafeed: false, - // Calendars - canGetCalendars: false, - canCreateCalendar: false, - canDeleteCalendar: false, - // Filters - canGetFilters: false, - canCreateFilter: false, - canDeleteFilter: false, - // File Data Visualizer - canFindFileStructure: false, - // Data Frame Analytics - canGetDataFrameAnalytics: false, - canDeleteDataFrameAnalytics: false, - canCreateDataFrameAnalytics: false, - canStartStopDataFrameAnalytics: false, - }; -} - -export interface PrivilegesResponse { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts new file mode 100644 index 0000000000000..1ca176d8d09ce --- /dev/null +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { hasLicenseExpired } from '../license'; + +import { MlCapabilities, getDefaultCapabilities } from '../../../common/types/capabilities'; +import { getCapabilities, getManageMlCapabilities } from './get_capabilities'; +import { ACCESS_DENIED_PATH } from '../management/management_urls'; + +let _capabilities: MlCapabilities = getDefaultCapabilities(); + +export function checkGetManagementMlJobsResolver() { + return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => { + getManageMlCapabilities().then( + ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { + _capabilities = capabilities; + // Loop through all capabilities to ensure they are all set to true. + const isManageML = Object.values(_capabilities).every(p => p === true); + + if (isManageML === true && isPlatinumOrTrialLicense === true) { + return resolve({ mlFeatureEnabledInSpace }); + } else { + window.location.href = ACCESS_DENIED_PATH; + return reject(); + } + } + ); + }); +} + +export function checkGetJobsCapabilitiesResolver(): Promise { + return new Promise((resolve, reject) => { + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; + // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. + // all other functionality is controlled by the return capabilities object. + // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, + // allow the promise to resolve as the separate license check will redirect then user to + // a basic feature + if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); + } else { + window.location.href = '#/access-denied'; + return reject(); + } + }); + }); +} + +export function checkCreateJobsCapabilitiesResolver(): Promise { + return new Promise((resolve, reject) => { + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; + // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, + // allow the promise to resolve as the separate license check will redirect then user to + // a basic feature + if (_capabilities.canCreateJob || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); + } else { + // if the user has no permission to create a job, + // redirect them back to the Transforms Management page + window.location.href = '#/jobs'; + return reject(); + } + }); + }); +} + +export function checkFindFileStructurePrivilegeResolver(): Promise { + return new Promise((resolve, reject) => { + getCapabilities().then(({ capabilities }) => { + _capabilities = capabilities; + // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. + // all other functionality is controlled by the return _capabilities object + if (_capabilities.canFindFileStructure) { + return resolve(_capabilities); + } else { + window.location.href = '#/access-denied'; + return reject(); + } + }); + }); +} + +// check the privilege type and the license to see whether a user has permission to access a feature. +// takes the name of the privilege variable as specified in get_privileges.js +export function checkPermission(capability: keyof MlCapabilities) { + const licenseHasExpired = hasLicenseExpired(); + return _capabilities[capability] === true && licenseHasExpired !== true; +} + +// create the text for the button's tooltips if the user's license has +// expired or if they don't have the privilege to press that button +export function createPermissionFailureMessage(privilegeType: keyof MlCapabilities) { + let message = ''; + const licenseHasExpired = hasLicenseExpired(); + if (licenseHasExpired) { + message = i18n.translate('xpack.ml.privilege.licenseHasExpiredTooltip', { + defaultMessage: 'Your license has expired.', + }); + } else if (privilegeType === 'canCreateJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.createMLJobsTooltip', { + defaultMessage: 'You do not have permission to create Machine Learning jobs.', + }); + } else if (privilegeType === 'canStartStopDatafeed') { + message = i18n.translate('xpack.ml.privilege.noPermission.startOrStopDatafeedsTooltip', { + defaultMessage: 'You do not have permission to start or stop datafeeds.', + }); + } else if (privilegeType === 'canUpdateJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.editJobsTooltip', { + defaultMessage: 'You do not have permission to edit jobs.', + }); + } else if (privilegeType === 'canDeleteJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.deleteJobsTooltip', { + defaultMessage: 'You do not have permission to delete jobs.', + }); + } else if (privilegeType === 'canCreateCalendar') { + message = i18n.translate('xpack.ml.privilege.noPermission.createCalendarsTooltip', { + defaultMessage: 'You do not have permission to create calendars.', + }); + } else if (privilegeType === 'canDeleteCalendar') { + message = i18n.translate('xpack.ml.privilege.noPermission.deleteCalendarsTooltip', { + defaultMessage: 'You do not have permission to delete calendars.', + }); + } else if (privilegeType === 'canForecastJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.runForecastsTooltip', { + defaultMessage: 'You do not have permission to run forecasts.', + }); + } + return i18n.translate('xpack.ml.privilege.pleaseContactAdministratorTooltip', { + defaultMessage: '{message} Please contact your administrator.', + values: { + message, + }, + }); +} diff --git a/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts new file mode 100644 index 0000000000000..c9561ed254544 --- /dev/null +++ b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts @@ -0,0 +1,40 @@ +/* + * 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 { ml } from '../services/ml_api_service'; + +import { setUpgradeInProgress } from '../services/upgrade_service'; +import { MlCapabilitiesResponse } from '../../../common/types/capabilities'; + +export function getCapabilities(): Promise { + return new Promise((resolve, reject) => { + ml.checkMlCapabilities() + .then((resp: MlCapabilitiesResponse) => { + if (resp.upgradeInProgress === true) { + setUpgradeInProgress(true); + } + resolve(resp); + }) + .catch(() => { + reject(); + }); + }); +} + +export function getManageMlCapabilities(): Promise { + return new Promise((resolve, reject) => { + ml.checkManageMLCapabilities() + .then((resp: MlCapabilitiesResponse) => { + if (resp.upgradeInProgress === true) { + setUpgradeInProgress(true); + } + resolve(resp); + }) + .catch(() => { + reject(); + }); + }); +} diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index b881bfe4f1fe6..7696143a9ce71 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -8,14 +8,14 @@ import React from 'react'; import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json'; import { getColumns } from './anomalies_table_columns'; -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => false, })); jest.mock('../../license', () => ({ hasLicenseExpired: () => false, })); -jest.mock('../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../services/field_format_service', () => ({ getFieldFormat: () => {}, diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 0c6c959927140..8f79ce4a6c08a 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -23,7 +23,7 @@ import { DetectorCell } from './detector_cell'; import { EntityCell } from '../entity_cell'; import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlFieldFormatService } from '../../services/field_format_service'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { formatValue } from '../../formatters/format_value'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 2a34f12330a75..5d2b8e35fbc0f 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { parseInterval } from '../../../../common/util/parse_interval'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts new file mode 100644 index 0000000000000..4bb670ad02dfc --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { EuiDataGridSorting } from '@elastic/eui'; + +import { multiColumnSortFactory } from './common'; + +describe('Transform: Define Pivot Common', () => { + test('multiColumnSortFactory()', () => { + const data = [ + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + ]; + + const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; + const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); + data.sort(multiColumnSort1); + + expect(data).toStrictEqual([ + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + ]); + + const sortingColumns2: EuiDataGridSorting['columns'] = [ + { id: 's', direction: 'asc' }, + { id: 'n', direction: 'desc' }, + ]; + const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); + data.sort(multiColumnSort2); + + expect(data).toStrictEqual([ + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + ]); + + const sortingColumns3: EuiDataGridSorting['columns'] = [ + { id: 'n', direction: 'desc' }, + { id: 's', direction: 'desc' }, + ]; + const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); + data.sort(multiColumnSort3); + + expect(data).toStrictEqual([ + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + ]); + }); +}); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts new file mode 100644 index 0000000000000..d141b68b5d03f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -0,0 +1,287 @@ +/* + * 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 moment from 'moment-timezone'; +import { useEffect, useMemo } from 'react'; + +import { + EuiDataGridCellValueElementProps, + EuiDataGridSorting, + EuiDataGridStyle, +} from '@elastic/eui'; + +import { + IndexPattern, + IFieldType, + ES_FIELD_TYPES, + KBN_FIELD_TYPES, +} from '../../../../../../../src/plugins/data/public'; + +import { + BASIC_NUMERICAL_TYPES, + EXTENDED_NUMERICAL_TYPES, +} from '../../data_frame_analytics/common/fields'; + +import { + FEATURE_IMPORTANCE, + FEATURE_INFLUENCE, + OUTLIER_SCORE, +} from '../../data_frame_analytics/common/constants'; +import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; +import { getNestedProperty } from '../../util/object_utils'; +import { mlFieldFormatService } from '../../services/field_format_service'; + +import { DataGridItem, IndexPagination, RenderCellValue } from './types'; + +export const INIT_MAX_COLUMNS = 20; + +export const euiDataGridStyle: EuiDataGridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + stripes: false, + rowHover: 'none', + header: 'shade', +}; + +export const euiDataGridToolbarSettings = { + showColumnSelector: true, + showStyleSelector: false, + showSortSelector: true, + showFullScreenSelector: false, +}; + +export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): string[] => { + const allFields = indexPattern.fields.map(f => f.name); + const indexPatternFields: string[] = allFields.filter(f => { + if (indexPattern.metaFields.includes(f)) { + return false; + } + + const fieldParts = f.split('.'); + const lastPart = fieldParts.pop(); + if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { + return false; + } + + return true; + }); + + return indexPatternFields; +}; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, resultsField: string) => { + return Object.keys(fieldTypes).map(field => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + const isSortable = true; + const type = fieldTypes[field]; + + const isNumber = + type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + if (isNumber) { + schema = 'numeric'; + } + + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; + } + + if ( + field === `${resultsField}.${OUTLIER_SCORE}` || + field.includes(`${resultsField}.${FEATURE_INFLUENCE}`) + ) { + schema = 'numeric'; + } + + if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) { + schema = 'json'; + } + + return { id: field, schema, isSortable }; + }); +}; + +export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case KBN_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case KBN_FIELD_TYPES.DATE: + schema = 'datetime'; + break; + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case KBN_FIELD_TYPES.NUMBER: + schema = 'numeric'; + break; + } + + return schema; +}; + +export const useRenderCellValue = ( + indexPattern: IndexPattern | undefined, + pagination: IndexPagination, + tableItems: DataGridItem[], + resultsField?: string, + cellPropsCallback?: ( + columnId: string, + cellValue: any, + fullItem: Record, + setCellProps: EuiDataGridCellValueElementProps['setCellProps'] + ) => void +): RenderCellValue => { + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: EuiDataGridCellValueElementProps['setCellProps']; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + if (indexPattern === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + function getCellValue(cId: string) { + if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) { + const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null); + return results[cId.replace(`${resultsField}.`, '')]; + } + + return tableItems.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(tableItems[adjustedRowIndex], cId, null) + : null; + } + + const cellValue = getCellValue(columnId); + + // React by default doesn't all us to use a hook in a callback. + // However, this one will be passed on to EuiDataGrid and its docs + // recommend wrapping `setCellProps` in a `useEffect()` hook + // so we're ignoring the linting rule here. + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (typeof cellPropsCallback === 'function') { + cellPropsCallback(columnId, cellValue, fullItem, setCellProps); + } + }, [columnId, cellValue]); + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + const field = indexPattern.fields.getByName(columnId); + if (field?.type === KBN_FIELD_TYPES.DATE) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]); + return renderCellValue; +}; + +/** + * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. + * `sortFn()` is recursive to support sorting on multiple columns. + * + * @param sortingColumns - The EUI data grid sorting configuration + * @returns The sorting function which can be used with an array's sort() function. + */ +export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { + const isString = (arg: any): arg is string => { + return typeof arg === 'string'; + }; + + const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { + const sort = sortingColumns[sortingColumnIndex]; + const aValue = getNestedProperty(a, sort.id, null); + const bValue = getNestedProperty(b, sort.id, null); + + if (typeof aValue === 'number' && typeof bValue === 'number') { + if (aValue < bValue) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (isString(aValue) && isString(bValue)) { + if (aValue.localeCompare(bValue) === -1) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue.localeCompare(bValue) === 1) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (sortingColumnIndex + 1 < sortingColumns.length) { + return sortFn(a, b, sortingColumnIndex + 1); + } + + return 0; + }; + + return sortFn; +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx new file mode 100644 index 0000000000000..a5b301902cc75 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -0,0 +1,181 @@ +/* + * 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, { useEffect, FC } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiCodeBlock, + EuiCopy, + EuiDataGrid, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { CoreSetup } from 'src/core/public'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; +import { UseIndexDataReturnType } from './types'; + +export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); + +interface PropsWithoutHeader extends UseIndexDataReturnType { + dataTestSubj: string; + toastNotifications: CoreSetup['notifications']['toasts']; +} + +interface PropsWithHeader extends PropsWithoutHeader { + copyToClipboard: string; + copyToClipboardDescription: string; + title: string; +} + +function isWithHeader(arg: any): arg is PropsWithHeader { + return typeof arg?.title === 'string' && arg?.title !== ''; +} + +type Props = PropsWithHeader | PropsWithoutHeader; + +export const DataGrid: FC = props => { + const { + columns, + dataTestSubj, + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems: data, + toastNotifications, + visibleColumns, + } = props; + + useEffect(() => { + if (invalidSortingColumnns.length > 0) { + invalidSortingColumnns.forEach(columnId => { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataGrid.invalidSortingColumnError', { + defaultMessage: `The column '{columnId}' cannot be used for sorting.`, + values: { columnId }, + }) + ); + }); + } + }, [invalidSortingColumnns, toastNotifications]); + + if (status === INDEX_STATUS.LOADED && data.length === 0) { + return ( +
+ {isWithHeader(props) && } + +

+ {i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', + })} +

+
+
+ ); + } + + if (noDataMessage !== '') { + return ( +
+ {isWithHeader(props) && } + +

{noDataMessage}

+
+
+ ); + } + + return ( +
+ {isWithHeader(props) && ( + + + + + + + {(copy: () => void) => ( + + )} + + + + )} + {status === INDEX_STATUS.ERROR && ( +
+ + + {errorMessage} + + + +
+ )} + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts new file mode 100644 index 0000000000000..2472878d1b0c1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { + getDataGridSchemasFromFieldTypes, + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useRenderCellValue, +} from './common'; +export { useDataGrid } from './use_data_grid'; +export { DataGrid } from './data_grid'; +export { + DataGridItem, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts new file mode 100644 index 0000000000000..5fa038edf7815 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -0,0 +1,98 @@ +/* + * 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 { Dispatch, SetStateAction } from 'react'; +import { SearchResponse } from 'elasticsearch'; + +import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { Dictionary } from '../../../../common/types/common'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; + +export type ColumnId = string; +export type DataGridItem = Record; + +export type IndexPagination = Pick; + +export type OnChangeItemsPerPage = (pageSize: any) => void; +export type OnChangePage = (pageIndex: any) => void; +export type OnSort = ( + sc: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> +) => void; + +export type RenderCellValue = ({ + rowIndex, + columnId, + setCellProps, +}: { + rowIndex: number; + columnId: string; + setCellProps: any; +}) => any; + +export type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +export interface SearchResponse7 extends SearchResponse { + hits: SearchResponse['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + +export interface UseIndexDataReturnType + extends Pick< + UseDataGridReturnType, + | 'errorMessage' + | 'invalidSortingColumnns' + | 'noDataMessage' + | 'onChangeItemsPerPage' + | 'onChangePage' + | 'onSort' + | 'pagination' + | 'setPagination' + | 'setVisibleColumns' + | 'rowCount' + | 'sortingColumns' + | 'status' + | 'tableItems' + | 'visibleColumns' + > { + columns: EuiDataGridColumn[]; + renderCellValue: RenderCellValue; +} + +export interface UseDataGridReturnType { + errorMessage: string; + invalidSortingColumnns: ColumnId[]; + noDataMessage: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onChangePage: OnChangePage; + onSort: OnSort; + pagination: IndexPagination; + resetPagination: () => void; + rowCount: number; + setErrorMessage: Dispatch>; + setNoDataMessage: Dispatch>; + setPagination: Dispatch>; + setRowCount: Dispatch>; + setSortingColumns: Dispatch>; + setStatus: Dispatch>; + setTableItems: Dispatch>; + setVisibleColumns: Dispatch>; + sortingColumns: EuiDataGridSorting['columns']; + status: INDEX_STATUS; + tableItems: DataGridItem[]; + visibleColumns: ColumnId[]; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts new file mode 100644 index 0000000000000..c7c4f46031b6e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts @@ -0,0 +1,112 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; + +import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { INIT_MAX_COLUMNS } from './common'; +import { + ColumnId, + DataGridItem, + IndexPagination, + OnChangeItemsPerPage, + OnChangePage, + OnSort, + UseDataGridReturnType, +} from './types'; + +export const useDataGrid = ( + columns: EuiDataGridColumn[], + defaultPageSize = 5, + defaultVisibleColumnsCount = INIT_MAX_COLUMNS, + defaultVisibleColumnsFilter?: (id: string) => boolean +): UseDataGridReturnType => { + const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; + + const [noDataMessage, setNoDataMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState([]); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState([]); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, []); + + const onChangePage: OnChangePage = useCallback( + pageIndex => setPagination(p => ({ ...p, pageIndex })), + [] + ); + + const resetPagination = () => setPagination(defaultPagination); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + const columnIds = columns.map(c => c.id); + const filteredColumnIds = + defaultVisibleColumnsFilter !== undefined + ? columnIds.filter(defaultVisibleColumnsFilter) + : columnIds; + const defaultVisibleColumns = filteredColumnIds.splice(0, defaultVisibleColumnsCount); + + useEffect(() => { + setVisibleColumns(defaultVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultVisibleColumns.join()]); + + const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); + + const onSort: OnSort = useCallback( + sc => { + // Check if an unsupported column type for sorting was selected. + const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { + const columnType = columns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + setInvalidSortingColumnns(updatedInvalidSortingColumnns); + if (updatedInvalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } + }, + [columns] + ); + + return { + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + resetPagination, + rowCount, + setErrorMessage, + setNoDataMessage, + setPagination, + setRowCount, + setSortingColumns, + setStatus, + setTableItems, + setVisibleColumns, + sortingColumns, + status, + tableItems, + visibleColumns, + }; +}; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index f8868ec099985..f259a1f1ffb02 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -30,7 +30,7 @@ import { import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { ConditionsSection } from './conditions_section'; import { ScopeSection } from './scope_section'; import { SelectRuleAction } from './select_rule_action'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index 5c43c558a3333..3e8a17eeb8617 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -45,7 +45,7 @@ jest.mock('../../services/job_service', () => ({ }, })); jest.mock('../../services/ml_api_service', () => 'ml'); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js index 8dca6db96d45b..48e0da72f067c 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js @@ -14,7 +14,7 @@ import React from 'react'; import { EuiCallOut, EuiCheckbox, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; import { ScopeExpression } from './scope_expression'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { getScopeFieldDefaults } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js index 3d7861fc7de0a..189fb6a35d3e6 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js @@ -11,7 +11,7 @@ jest.mock('../../services/job_service.js', () => 'mlJobService'); // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts new file mode 100644 index 0000000000000..51b2918012c8d --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 const DEFAULT_RESULTS_FIELD = 'ml'; +export const FEATURE_IMPORTANCE = 'feature_importance'; +export const FEATURE_INFLUENCE = 'feature_influence'; +export const OUTLIER_SCORE = 'outlier_score'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts deleted file mode 100644 index 2b6d733837562..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts +++ /dev/null @@ -1,23 +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 { EuiDataGridStyle } from '@elastic/eui'; - -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'none', - header: 'shade', -}; - -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index f165669bdd674..8423bc1b94a09 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getNestedProperty } from '../../util/object_utils'; import { - DataFrameAnalyticsConfig, getNumTopFeatureImportanceValues, getPredictedFieldName, getDependentVar, getPredictionFieldName, + isClassificationAnalysis, + isOutlierAnalysis, + isRegressionAnalysis, + DataFrameAnalyticsConfig, } from './analytics'; import { Field } from '../../../../common/types/fields'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE } from './constants'; + export type EsId = string; export type EsDocSource = Record; export type EsFieldName = string; @@ -42,7 +46,7 @@ export const EXTENDED_NUMERICAL_TYPES = new Set([ ES_FIELD_TYPES.SCALED_FLOAT, ]); -const ML__ID_COPY = 'ml__id_copy'; +export const ML__ID_COPY = 'ml__id_copy'; export const isKeywordAndTextType = (fieldName: string): boolean => { const { fields } = newJobCapsService; @@ -64,32 +68,61 @@ export const isKeywordAndTextType = (fieldName: string): boolean => { }; // Used to sort columns: +// - Anchor on the left ml.outlier_score, ml.is_training, , // - string based columns are moved to the left -// - followed by the outlier_score column -// - feature_influence fields get moved next to the corresponding field column +// - feature_influence/feature_importance fields get moved next to the corresponding field column // - overall fields get sorted alphabetically -export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: string, b: string) => { - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; +export const sortExplorationResultsFields = ( + a: string, + b: string, + jobConfig: DataFrameAnalyticsConfig +) => { + const resultsField = jobConfig.dest.results_field; - if (typeofA !== 'string' && typeofB === 'string') { - return 1; - } - if (typeofA === 'string' && typeofB !== 'string') { - return -1; - } - if (typeofA === 'string' && typeofB === 'string') { - return a.localeCompare(b); - } + if (isOutlierAnalysis(jobConfig.analysis)) { + if (a === `${resultsField}.${OUTLIER_SCORE}`) { + return -1; + } - if (a === `${resultsField}.outlier_score`) { - return -1; + if (b === `${resultsField}.${OUTLIER_SCORE}`) { + return 1; + } } - if (b === `${resultsField}.outlier_score`) { - return 1; + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); + + if (a === `${resultsField}.is_training`) { + return -1; + } + if (b === `${resultsField}.is_training`) { + return 1; + } + if (a === predictedField) { + return -1; + } + if (b === predictedField) { + return 1; + } + if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { + return -1; + } + if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { + return 1; + } + + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + if (b === `${resultsField}.prediction_probability`) { + return 1; + } } + const typeofA = typeof a; + const typeofB = typeof b; + const tokensA = a.split('.'); const prefixA = tokensA[0]; const tokensB = b.split('.'); @@ -109,91 +142,6 @@ export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: strin return a.localeCompare(tokensB.join('.')); } - return a.localeCompare(b); -}; - -export const sortRegressionResultsFields = ( - a: string, - b: string, - jobConfig: DataFrameAnalyticsConfig -) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - if (a === `${resultsField}.is_training`) { - return -1; - } - if (b === `${resultsField}.is_training`) { - return 1; - } - if (a === predictedField) { - return -1; - } - if (b === predictedField) { - return 1; - } - if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { - return -1; - } - if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - - return a.localeCompare(b); -}; - -// Used to sort columns: -// Anchor on the left ml.is_training, , -export const sortRegressionResultsColumns = ( - obj: EsDocSource, - jobConfig: DataFrameAnalyticsConfig -) => (a: string, b: string) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; - - if (a === `${resultsField}.is_training`) { - return -1; - } - - if (b === `${resultsField}.is_training`) { - return 1; - } - - if (a === predictedField) { - return -1; - } - - if (b === predictedField) { - return 1; - } - - if (a === dependentVariable) { - return -1; - } - - if (b === dependentVariable) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - if (typeofA !== 'string' && typeofB === 'string') { return 1; } @@ -204,44 +152,9 @@ export const sortRegressionResultsColumns = ( return a.localeCompare(b); } - const tokensA = a.split('.'); - const prefixA = tokensA[0]; - const tokensB = b.split('.'); - const prefixB = tokensB[0]; - - if (prefixA === resultsField && tokensA.length > 1 && prefixB !== resultsField) { - tokensA.shift(); - tokensA.shift(); - if (tokensA.join('.') === b) return 1; - return tokensA.join('.').localeCompare(b); - } - - if (prefixB === resultsField && tokensB.length > 1 && prefixA !== resultsField) { - tokensB.shift(); - tokensB.shift(); - if (tokensB.join('.') === a) return -1; - return a.localeCompare(tokensB.join('.')); - } - return a.localeCompare(b); }; -export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFieldName[] { - const flatDocFields: EsFieldName[] = []; - const newDocFields = Object.keys(obj); - newDocFields.forEach(f => { - const fieldValue = getNestedProperty(obj, f); - if (typeof fieldValue !== 'object' || fieldValue === null || Array.isArray(fieldValue)) { - flatDocFields.push(f); - } else { - const innerFields = getFlattenedFields(fieldValue, resultsField); - const flattenedFields = innerFields.map(d => `${f}.${d}`); - flatDocFields.push(...flattenedFields); - } - }); - return flatDocFields.filter(f => f !== ML__ID_COPY); -} - export const getDefaultFieldsFromJobCaps = ( fields: Field[], jobConfig: DataFrameAnalyticsConfig, @@ -259,49 +172,72 @@ export const getDefaultFieldsFromJobCaps = ( return fieldsObj; } - const dependentVariable = getDependentVar(jobConfig.analysis); - const type = newJobCapsService.getFieldById(dependentVariable)?.type; - const predictionFieldName = getPredictionFieldName(jobConfig.analysis); - const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); // default is 'ml' const resultsField = jobConfig.dest.results_field; - const defaultPredictionField = `${dependentVariable}_prediction`; - const predictedField = `${resultsField}.${ - predictionFieldName ? predictionFieldName : defaultPredictionField - }`; - const featureImportanceFields = []; - - if ((numTopFeatureImportanceValues ?? 0) > 0) { - featureImportanceFields.push({ - id: `${resultsField}.feature_importance`, - name: `${resultsField}.feature_importance`, - type: KBN_FIELD_TYPES.NUMBER, - }); + const featureInfluenceFields = []; + const allFields: any = []; + let type: ES_FIELD_TYPES | undefined; + let predictedField: string | undefined; + + if (isOutlierAnalysis(jobConfig.analysis)) { + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push({ + id: `${resultsField}.${OUTLIER_SCORE}`, + name: `${resultsField}.${OUTLIER_SCORE}`, + type: KBN_FIELD_TYPES.NUMBER, + }); + + featureInfluenceFields.push( + ...fields + .filter(d => !jobConfig.analyzed_fields.excludes.includes(d.id)) + .map(d => ({ + id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`, + name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`, + type: KBN_FIELD_TYPES.NUMBER, + })) + ); + } } - let allFields: any = []; - // Only need to add these fields if we didn't use dest index pattern to get the fields - if (needsDestIndexFields === true) { - allFields.push( - { - id: `${resultsField}.is_training`, - name: `${resultsField}.is_training`, - type: ES_FIELD_TYPES.BOOLEAN, - }, - { id: predictedField, name: predictedField, type } - ); + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + type = newJobCapsService.getFieldById(dependentVariable)?.type; + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); + + const defaultPredictionField = `${dependentVariable}_prediction`; + predictedField = `${resultsField}.${ + predictionFieldName ? predictionFieldName : defaultPredictionField + }`; + + if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { + featureImportanceFields.push({ + id: `${resultsField}.${FEATURE_IMPORTANCE}`, + name: `${resultsField}.${FEATURE_IMPORTANCE}`, + type: KBN_FIELD_TYPES.UNKNOWN, + }); + } + + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push( + { + id: `${resultsField}.is_training`, + name: `${resultsField}.is_training`, + type: ES_FIELD_TYPES.BOOLEAN, + }, + { id: predictedField, name: predictedField, type } + ); + } } - allFields.push(...fields, ...featureImportanceFields); + allFields.push(...fields, ...featureImportanceFields, ...featureInfluenceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => - sortRegressionResultsFields(a, b, jobConfig) + sortExplorationResultsFields(a, b, jobConfig) ); - // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid - if (needsDestIndexFields === false) { - allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); - } let selectedFields = allFields.filter( (field: any) => field.name === predictedField || !field.name.includes('.keyword') @@ -317,145 +253,3 @@ export const getDefaultFieldsFromJobCaps = ( depVarType: type, }; }; - -export const getDefaultClassificationFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - const resultsField = jobConfig.dest.results_field; - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - - if (k === `${resultsField}.prediction_probability`) { - return true; - } - - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultRegressionFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - const resultsField = jobConfig.dest.results_field; - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultSelectableFields = (docs: EsDoc[], resultsField: string): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields.filter(k => { - if (k === `${resultsField}.outlier_score`) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }); -}; - -export const toggleSelectedFieldSimple = ( - selectedFields: EsFieldName[], - column: EsFieldName -): EsFieldName[] => { - const index = selectedFields.indexOf(column); - - if (index === -1) { - selectedFields.push(column); - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; -// Fields starting with 'ml' or custom result name not included in newJobCapsService fields so -// need to recreate the field with correct type and add to selected fields -export const toggleSelectedField = ( - selectedFields: Field[], - column: EsFieldName, - resultsField: string, - depVarType?: ES_FIELD_TYPES -): Field[] => { - const index = selectedFields.map(field => field.name).indexOf(column); - if (index === -1) { - const columnField = newJobCapsService.getFieldById(column); - if (columnField !== null) { - selectedFields.push(columnField); - } else { - const resultFieldPattern = `^${resultsField}\.`; - const regex = new RegExp(resultFieldPattern); - const isResultField = column.match(regex) !== null; - let newField; - - if (isResultField && column.includes('is_training')) { - newField = { - id: column, - name: column, - type: ES_FIELD_TYPES.BOOLEAN, - }; - } else if (isResultField && depVarType !== undefined) { - newField = { - id: column, - name: column, - type: depVarType, - }; - } - - if (newField) selectedFields.push(newField); - } - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts new file mode 100644 index 0000000000000..87b8c15aeaa78 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -0,0 +1,68 @@ +/* + * 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 { getErrorMessage } from '../../../../common/util/errors'; + +import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid'; +import { ml } from '../../services/ml_api_service'; + +import { isKeywordAndTextType } from '../common/fields'; +import { SavedSearchQuery } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig, INDEX_STATUS } from './analytics'; + +export const getIndexData = async ( + jobConfig: DataFrameAnalyticsConfig | undefined, + dataGrid: UseDataGridReturnType, + searchQuery: SavedSearchQuery +) => { + if (jobConfig !== undefined) { + const { + pagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + } = dataGrid; + + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const { pageIndex, pageSize } = pagination; + const resp: SearchResponse7 = await ml.esSearch({ + index: jobConfig.dest.index, + body: { + query: searchQuery, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }); + + setRowCount(resp.hits.total.value); + + const docs = resp.hits.hits.map(d => d._source); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts new file mode 100644 index 0000000000000..12ae4a586e949 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts @@ -0,0 +1,49 @@ +/* + * 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 { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { newJobCapsService } from '../../services/new_job_capabilities_service'; + +import { getDefaultFieldsFromJobCaps, DataFrameAnalyticsConfig } from '../common'; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getIndexFields = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + needsDestIndexFields: boolean +) => { + const { fields } = newJobCapsService; + if (jobConfig !== undefined) { + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: FieldTypes = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + return { + defaultSelectedFields: defaultSelected.map(field => field.id), + fieldTypes: types, + tableFields: allFields, + }; + } else { + return { + defaultSelectedFields: [], + fieldTypes: {}, + tableFields: [], + }; + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 7b76faf613ce8..400902c152c9e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -30,16 +30,8 @@ export { } from './analytics'; export { - getDefaultSelectableFields, - getDefaultRegressionFields, - getDefaultClassificationFields, getDefaultFieldsFromJobCaps, - getFlattenedFields, - sortColumns, - sortRegressionResultsColumns, - sortRegressionResultsFields, - toggleSelectedField, - toggleSelectedFieldSimple, + sortExplorationResultsFields, EsId, EsDoc, EsDocSource, @@ -47,4 +39,7 @@ export { MAX_COLUMNS, } from './fields'; -export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; +export { getIndexData } from './get_index_data'; +export { getIndexFields } from './get_index_fields'; + +export { useResultsViewConfig } from './use_results_view_config'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts new file mode 100644 index 0000000000000..0bc9e78207596 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -0,0 +1,104 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; + +import { getErrorMessage } from '../../../../common/util/errors'; + +import { getIndexPatternIdFromName } from '../../util/index_utils'; +import { ml } from '../../services/ml_api_service'; +import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { useMlContext } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig } from '../common'; + +import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics'; +import { DATA_FRAME_TASK_STATE } from '../pages/analytics_management/components/analytics_list/common'; + +export const useResultsViewConfig = (jobId: string) => { + const mlContext = useMlContext(); + const [indexPattern, setIndexPattern] = useState(undefined); + const [isInitialized, setIsInitialized] = useState(false); + const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); + const [jobConfig, setJobConfig] = useState(undefined); + const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( + undefined + ); + const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); + + // get analytics configuration, index pattern and field caps + useEffect(() => { + (async function() { + setIsLoadingJobConfig(false); + + try { + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + const jobConfigUpdate = analyticsConfigs.data_frame_analytics[0]; + + try { + const destIndex = Array.isArray(jobConfigUpdate.dest.index) + ? jobConfigUpdate.dest.index[0] + : jobConfigUpdate.dest.index; + const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; + let indexP: IndexPattern | undefined; + + try { + indexP = await mlContext.indexPatterns.get(destIndexPatternId); + } catch (e) { + indexP = undefined; + } + + if (indexP === undefined) { + const sourceIndex = jobConfigUpdate.source.index[0]; + const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } + + if (indexP !== undefined) { + await newJobCapsService.initializeFromIndexPattern(indexP, false, false); + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIndexPattern(indexP); + setIsInitialized(true); + setIsLoadingJobConfig(false); + } + } catch (e) { + setJobCapsServiceErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + } + } catch (e) { + setJobConfigErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + })(); + }, []); + + return { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 5c151166829ab..ccac9a697210b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -4,183 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for classification job ID {jobId}', - values: { jobId }, - })} - - -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const ClassificationExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } else { - setJobConfigErrorMessage( - i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage', - { - defaultMessage: 'No results found.', - } - ) - ); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
- ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - + - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + EvaluatePanel={EvaluatePanel} + /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx deleted file mode 100644 index 424fc002795ca..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ClassificationExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx deleted file mode 100644 index bf63dfe68fe9e..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ /dev/null @@ -1,235 +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 React, { Fragment, FC, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiProgress, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; - -import { - DataFrameAnalyticsConfig, - MAX_COLUMNS, - INDEX_STATUS, - SEARCH_SIZE, - defaultSearchQuery, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData } from './use_explore_data'; // TableItem -import { ExplorationTitle } from './classification_exploration'; -import { ClassificationExplorationDataGrid } from './classification_exploration_data_grid'; -import { ExplorationQueryBar } from '../exploration_query_bar'; - -const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText', - { - defaultMessage: 'Showing documents for which predictions exist', - } -); - -const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText', - { - defaultMessage: 'Showing first {searchSize} documents for which predictions exist', - values: { searchSize: SEARCH_SIZE }, - } -); - -interface Props { - indexPattern: IndexPattern; - jobConfig: DataFrameAnalyticsConfig; - jobStatus?: DATA_FRAME_TASK_STATE; - setEvaluateSearchQuery: React.Dispatch>; -} - -export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); - - useEffect(() => { - setEvaluateSearchQuery(searchQuery); - }, [JSON.stringify(searchQuery)]); - - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; - - if (jobConfig === undefined) { - return null; - } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { - return ( - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - -

{errorMessage}

-
-
- ); - } - - return ( - - - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - - - - - {docFieldsCount > MAX_COLUMNS && ( - - {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - } - )} - - )} - - - - - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - - - - - - - - - - - - - - - - )} - - ); - } -); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts deleted file mode 100644 index c8809ca5e471b..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts +++ /dev/null @@ -1,292 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, - LoadExploreDataArg, -} from '../../../../common/analytics'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx index 9765192f0e446..839587c47289a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx @@ -15,7 +15,7 @@ interface Props { export const ErrorCallout: FC = ({ error }) => { let errorCallout = ( = ({ error }) => { if (error.includes('index_not_found')) { errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noIndexCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the destination index exists and contains documents.', })} @@ -46,16 +46,13 @@ export const ErrorCallout: FC = ({ error }) => { // Job was started but no results have been written yet errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noDataCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the job has completed and the index contains documents.', })} @@ -66,22 +63,16 @@ export const ErrorCallout: FC = ({ error }) => { // query bar syntax is incorrect errorCallout = (

- {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody', - { - defaultMessage: - 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', - } - )} + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorBody', { + defaultMessage: + 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', + })}

); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx deleted file mode 100644 index e88bc1cd06f95..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx +++ /dev/null @@ -1,161 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const FEATURE_INFLUENCE = 'feature_influence'; -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ExplorationDataGrid: FC = ({ - colorRange, - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - const split = columnId.split('.'); - let backgroundColor; - - // column with feature values get color coded by its corresponding influencer value - if (fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { - backgroundColor = colorRange(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`]); - } - - // column with influencer values get color coded by its own value - if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { - backgroundColor = colorRange(cellValue); - } - - if (backgroundColor !== undefined) { - setCellProps({ - style: { backgroundColor }, - }); - } - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts deleted file mode 100644 index ea89e91de5046..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { ExplorationDataGrid } from './exploration_data_grid'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx new file mode 100644 index 0000000000000..1986c486974c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -0,0 +1,76 @@ +/* + * 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, { FC, useState } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { useResultsViewConfig, DataFrameAnalyticsConfig } from '../../../../common'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; + +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { ExplorationResultsTable } from '../exploration_results_table'; +import { JobConfigErrorCallout } from '../job_config_error_callout'; +import { LoadingPanel } from '../loading_panel'; + +export interface EvaluatePanelProps { + jobConfig: DataFrameAnalyticsConfig; + jobStatus?: DATA_FRAME_TASK_STATE; + searchQuery: ResultsSearchQuery; +} + +interface Props { + jobId: string; + title: string; + EvaluatePanel: FC; +} + +export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel }) => { + const { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { + return ( + + ); + } + + return ( + <> + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( + + )} + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && + jobConfig !== undefined && + indexPattern !== undefined && + isInitialized === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts new file mode 100644 index 0000000000000..bf294a3cd08c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { EvaluatePanelProps, ExplorationPageWrapper } from './exploration_page_wrapper'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx new file mode 100644 index 0000000000000..24e5785c6e808 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -0,0 +1,172 @@ +/* + * 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, { Fragment, FC, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; + +import { + DataFrameAnalyticsConfig, + MAX_COLUMNS, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, +} from '../../../../common'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { ExplorationTitle } from '../exploration_title'; +import { ExplorationQueryBar } from '../exploration_query_bar'; + +import { useExplorationResults } from './use_exploration_results'; + +const showingDocs = i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', + { + defaultMessage: 'Showing documents for which predictions exist', + } +); + +const showingFirstDocs = i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText', + { + defaultMessage: 'Showing first {searchSize} documents for which predictions exist', + values: { searchSize: SEARCH_SIZE }, + } +); + +interface Props { + indexPattern: IndexPattern; + jobConfig: DataFrameAnalyticsConfig; + jobStatus?: DATA_FRAME_TASK_STATE; + setEvaluateSearchQuery: React.Dispatch>; + title: string; +} + +export const ExplorationResultsTable: FC = React.memo( + ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + useEffect(() => { + setEvaluateSearchQuery(searchQuery); + }, [JSON.stringify(searchQuery)]); + + const classificationData = useExplorationResults(indexPattern, jobConfig, searchQuery); + const docFieldsCount = classificationData.columns.length; + const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; + + if (jobConfig === undefined || classificationData === undefined) { + return null; + } + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + return ( + + + + + + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} + + +

{errorMessage}

+
+
+ ); + } + + return ( + + + + + + + + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} + + + + + + {docFieldsCount > MAX_COLUMNS && ( + + {i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.fieldSelection', + { + defaultMessage: + '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', + values: { selectedFieldsLength: visibleColumns.length, docFieldsCount }, + } + )} + + )} + + + + + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + + + + + + + + + + + + + + + + + )} + + ); + } +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts new file mode 100644 index 0000000000000..19308640c8b02 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ExplorationResultsTable } from './exploration_results_table'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts new file mode 100644 index 0000000000000..6f9dc694d8172 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -0,0 +1,72 @@ +/* + * 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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_IMPORTANCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +export const useExplorationResults = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-importance columns and the doc id copy + d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== ML__ID_COPY + ); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx new file mode 100644 index 0000000000000..f06c88c73df71 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx @@ -0,0 +1,15 @@ +/* + * 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, { FC } from 'react'; + +import { EuiTitle } from '@elastic/eui'; + +export const ExplorationTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts new file mode 100644 index 0000000000000..b34e61b3b5e76 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ExplorationTitle } from './exploration_title'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts new file mode 100644 index 0000000000000..a5991f4325d12 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { JobConfigErrorCallout } from './job_config_error_callout'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx new file mode 100644 index 0000000000000..945d6654067c0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx @@ -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 React, { FC } from 'react'; + +import { EuiCallOut, EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ExplorationTitle } from '../exploration_title'; + +const jobConfigErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobConfig.errorTitle', { + defaultMessage: 'Unable to fetch results. An error occurred loading the job configuration data.', +}); + +const jobCapsErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobCaps.errorTitle', { + defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", +}); + +interface Props { + jobCapsServiceErrorMessage: string | undefined; + jobConfigErrorMessage: string | undefined; + title: string; +} + +export const JobConfigErrorCallout: FC = ({ + jobCapsServiceErrorMessage, + jobConfigErrorMessage, + title, +}) => { + return ( + + + + +

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts similarity index 100% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts new file mode 100644 index 0000000000000..bfd3dd33995aa --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -0,0 +1,23 @@ +/* + * 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 { DataGridItem } from '../../../../../components/data_grid'; + +import { DataFrameAnalyticsConfig } from '../../../../common'; +import { FEATURE_INFLUENCE, OUTLIER_SCORE } from '../../../../common/constants'; + +export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => + `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; + +export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] = []) => { + if (tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) + ).length; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index fdcb7d9d237e3..0154f92576c4a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -15,127 +15,51 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { useColorRange, - ColorRangeLegend, COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; +import { ColorRangeLegend } from '../../../../../components/color_range_legend'; +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; -import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common'; +import { defaultSearchQuery, useResultsViewConfig, INDEX_STATUS } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { useExploreData, TableItem } from '../../hooks/use_explore_data'; - -import { ExplorationDataGrid } from '../exploration_data_grid'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { ExplorationTitle } from '../exploration_title'; -const FEATURE_INFLUENCE = 'feature_influence'; +import { getFeatureCount } from './common'; +import { useOutlierData } from './use_outlier_data'; -const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { - defaultMessage: 'Outlier detection job ID {jobId}', - values: { jobId }, - })} - - -); +export type TableItem = Record; interface ExplorationProps { jobId: string; } -const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { - if (tableItems.length === 0) { - return 0; - } - - return Object.keys(tableItems[0]).filter(key => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; -}; - export const OutlierExploration: FC = React.memo(({ jobId }) => { - const { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - searchQuery, - selectedFields, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - rowCount, - status, - tableFields, - tableItems, - } = useExploreData(jobId); - - const columns = []; - - if ( - jobConfig !== undefined && - indexPattern !== undefined && - selectedFields.length > 0 && - tableItems.length > 0 - ) { - const resultsField = jobConfig.dest.results_field; - const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); - columns.push( - ...tableFields.sort(sortColumns(tableItems[0], resultsField)).map(id => { - const idWithoutPrefix = id.replace(removePrefix, ''); - const field = indexPattern.fields.getByName(idWithoutPrefix); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'number': - schema = 'numeric'; - break; - } - - if (id === `${resultsField}.outlier_score`) { - schema = 'numeric'; - } - - return { id, schema }; - }) - ); - } + const explorationTitle = i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { + defaultMessage: 'Outlier detection job ID {jobId}', + values: { jobId }, + }); - const colorRange = useColorRange( - COLOR_RANGE.BLUE, - COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 - ); + const { indexPattern, jobConfig, jobStatus } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); - if (jobConfig === undefined || indexPattern === undefined) { - return null; - } + const { columns, errorMessage, status, tableItems } = outlierData; // if it's a searchBar syntax error leave the table visible so they can try again if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( - + = React.memo(({ jobId }) = ); } - let tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : undefined; - - if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) { - tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', - }); - } + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 + ); return ( @@ -170,7 +88,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = gutterSize="s" > - + {jobStatus !== undefined && ( @@ -179,7 +97,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && indexPattern !== undefined && ( <> @@ -200,19 +118,10 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = {columns.length > 0 && tableItems.length > 0 && ( - )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts new file mode 100644 index 0000000000000..91a5ba2db6908 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { DataFrameAnalyticsConfig } from '../../../../common'; + +import { getOutlierScoreFieldName } from './common'; + +describe('Data Frame Analytics: common utils', () => { + test('getOutlierScoreFieldName()', () => { + const jobConfig: DataFrameAnalyticsConfig = { + id: 'the-id', + analysis: { outlier_detection: {} }, + dest: { + index: 'the-dest-index', + results_field: 'the-results-field', + }, + source: { + index: 'the-source-index', + }, + analyzed_fields: { includes: [], excludes: [] }, + model_memory_limit: '50mb', + create_time: 1234, + version: '1.0.0', + }; + + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + + expect(outlierScoreFieldName).toMatch('the-results-field.outlier_score'); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts new file mode 100644 index 0000000000000..0d06bc0d43307 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -0,0 +1,117 @@ +/* + * 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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + useColorRange, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from '../../../../../components/color_range_legend'; +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +import { getFeatureCount, getOutlierScoreFieldName } from './common'; + +export const useOutlierData = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined && indexPattern !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-influence columns and the doc id copy + d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY + ); + + // initialize sorting: reverse sort on outlier score column + useEffect(() => { + if (jobConfig !== undefined) { + dataGrid.setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); + } + }, [jobConfig && jobConfig.id]); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, dataGrid.tableItems) : 1 + ); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD, + (columnId, cellValue, fullItem, setCellProps) => { + const resultsField = jobConfig?.dest.results_field ?? ''; + + const split = columnId.split('.'); + let backgroundColor; + + // column with feature values get color coded by its corresponding influencer value + if ( + fullItem[resultsField] !== undefined && + fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`] !== undefined + ) { + backgroundColor = colorRange(fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`]); + } + + // column with influencer values get color coded by its own value + if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { + backgroundColor = colorRange(cellValue); + } + + if (backgroundColor !== undefined) { + setCellProps({ + style: { backgroundColor }, + }); + } + } + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 6ef6666be5ec6..f6e8e0047671f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -235,7 +235,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} > {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink', + 'xpack.ml.dataframe.analytics.regressionExploration.regressionDocsLink', { defaultMessage: 'Regression evaluation docs ', } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index bfeca76a2b1c7..36d91f6f41d44 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -4,174 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for regression job ID {jobId}', - values: { jobId }, - })} - - -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const RegressionExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IIndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
- ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - - )} - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx deleted file mode 100644 index 0fcb1ed600719..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const RegressionExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx deleted file mode 100644 index 43fa50b2e4df5..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ /dev/null @@ -1,233 +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 React, { Fragment, FC, useEffect } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiProgress, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; - -import { - DataFrameAnalyticsConfig, - MAX_COLUMNS, - INDEX_STATUS, - SEARCH_SIZE, - defaultSearchQuery, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { useExploreData } from './use_explore_data'; -import { ExplorationTitle } from './regression_exploration'; -import { RegressionExplorationDataGrid } from './regression_exploration_data_grid'; -import { ExplorationQueryBar } from '../exploration_query_bar'; - -const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', - { - defaultMessage: 'Showing documents for which predictions exist', - } -); - -const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText', - { - defaultMessage: 'Showing first {searchSize} documents for which predictions exist', - values: { searchSize: SEARCH_SIZE }, - } -); - -interface Props { - indexPattern: IndexPattern; - jobConfig: DataFrameAnalyticsConfig; - jobStatus?: DATA_FRAME_TASK_STATE; - setEvaluateSearchQuery: React.Dispatch>; -} - -export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); - - useEffect(() => { - setEvaluateSearchQuery(searchQuery); - }, [JSON.stringify(searchQuery)]); - - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; - - if (jobConfig === undefined) { - return null; - } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { - return ( - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - -

{errorMessage}

-
-
- ); - } - - return ( - - - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - - - - - {docFieldsCount > MAX_COLUMNS && ( - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - } - )} - - )} - - - - - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - - - - - - - - - - - - - - - - )} - - ); - } -); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts deleted file mode 100644 index 978aafd10de11..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ /dev/null @@ -1,291 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - LoadExploreDataArg, - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, -} from '../../../../common/analytics'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts deleted file mode 100644 index 48db8e34c934e..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts +++ /dev/null @@ -1,12 +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 { DataFrameAnalyticsConfig } from '../../../../common'; - -const OUTLIER_SCORE = 'outlier_score'; - -export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => - `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts deleted file mode 100644 index dd896ca02f7f7..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { useExploreData, TableItem } from './use_explore_data'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts deleted file mode 100644 index a0a9eb8312499..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts +++ /dev/null @@ -1,267 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { Dictionary } from '../../../../../../../common/types/common'; - -import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { ml } from '../../../../../services/ml_api_service'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; - -import { - getDefaultSelectableFields, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, - MAX_COLUMNS, - defaultSearchQuery, -} from '../../../../common'; -import { isKeywordAndTextType } from '../../../../common/fields'; - -import { getOutlierScoreFieldName } from './common'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; - -export type TableItem = Record; - -type Pagination = Pick; - -interface UseExploreDataReturnType { - errorMessage: string; - indexPattern: IndexPattern | undefined; - jobConfig: DataFrameAnalyticsConfig | undefined; - jobStatus: DATA_FRAME_TASK_STATE | undefined; - pagination: Pagination; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setJobConfig: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = (jobId: string): UseExploreDataReturnType => { - const mlContext = useMlContext(); - - const [indexPattern, setIndexPattern] = useState(undefined); - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [sortingColumns, setSortingColumns] = useState([]); - - // get analytics configuration - useEffect(() => { - (async function() { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - } - })(); - }, []); - - // get index pattern and field caps - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - } catch (e) { - // eslint-disable-next-line - console.log('Error loading index field data', e); - } - } - })(); - }, [jobConfig && jobConfig.id]); - - // initialize sorting: reverse sort on outlier score column - useEffect(() => { - if (jobConfig !== undefined) { - setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); - } - }, [jobConfig && jobConfig.id]); - - // update data grid data - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const { pageIndex, pageSize } = pagination; - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query: searchQuery, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultSelectableFields(docs, resultsField); - setSelectedFields(newSelectedFields.sort().splice(0, MAX_COLUMNS)); - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableFields(flattenedFields); - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - })(); - }, [jobConfig && jobConfig.id, pagination, searchQuery, selectedFields, sortingColumns]); - - return { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - rowCount, - searchQuery, - selectedFields, - setJobConfig, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index eb1871c98764b..8c65af1d92959 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -214,7 +215,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo }, results_field: { optional: true, - defaultValue: 'ml', + defaultValue: DEFAULT_RESULTS_FIELD, }, }, model_memory_limit: { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 6d1db5033863b..3e5224b76329e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { render } from '@testing-library/react'; -import * as CheckPrivilige from '../../../../../privilege/check_privilege'; +import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; import { DeleteAction } from './action_delete'; import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; -jest.mock('../../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index 47fc84cf450c0..2923938ae68ac 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -19,7 +19,7 @@ import { deleteAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx index 40664a1413845..bbbda85c45e49 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx @@ -19,7 +19,7 @@ import { startAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 4e19df9ae22a8..72514c91ff58b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -12,7 +12,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { getAnalysisType, 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 d2e5f582d23f6..58bc75bd7309b 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 @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { getTaskStateBadge } from './columns'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index e5054e8a6ad2c..3c8c3c3b3aa55 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { createPermissionFailureMessage } from '../../../../../privilege/check_privilege'; +import { createPermissionFailureMessage } from '../../../../../capabilities/check_capabilities'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; export const CreateAnalyticsButton: FC = props => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index e121268e65e86..70840a442f6f6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -6,7 +6,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap index 46428ff9c351a..59481cb086566 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap @@ -70,6 +70,7 @@ exports[`Overrides render overrides 1`] = ` "asPlainText": true, } } + sortMatchesBy="none" />
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx index dddf64ce2cfd3..c05ca63df3a75 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { ml } from '../../../../services/ml_api_service'; import { isFullLicense } from '../../../../license'; -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { useMlKibana } from '../../../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 3a37274edbc16..86ffc4a2614b9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -33,7 +33,7 @@ import { NavigationMenu } from '../../components/navigation_menu'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isFullLicense } from '../../license'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap index 1b27d09965c4e..06ee16f264756 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap @@ -150,6 +150,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe placeholder="Select entities" selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> = ( const check = async () => { try { - const checkPrivilege = await checkGetManagementMlJobs(); + const checkPrivilege = await checkGetManagementMlJobsResolver(); setInitialized(true); setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); } catch (e) { diff --git a/x-pack/plugins/ml/public/application/overview/overview_page.tsx b/x-pack/plugins/ml/public/application/overview/overview_page.tsx index 7c995c8a568d7..9a852c491ee27 100644 --- a/x-pack/plugins/ml/public/application/overview/overview_page.tsx +++ b/x-pack/plugins/ml/public/application/overview/overview_page.tsx @@ -6,7 +6,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui'; -import { checkPermission } from '../privilege/check_privilege'; +import { checkPermission } from '../capabilities/check_capabilities'; import { mlNodesAvailable } from '../ml_nodes_check/check_ml_nodes'; import { NavigationMenu } from '../components/navigation_menu'; import { OverviewSideBar } from './components/sidebar'; diff --git a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts b/x-pack/plugins/ml/public/application/privilege/check_privilege.ts deleted file mode 100644 index 4de8c6eb703ff..0000000000000 --- a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts +++ /dev/null @@ -1,141 +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 { i18n } from '@kbn/i18n'; - -import { hasLicenseExpired } from '../license'; - -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { getPrivileges, getManageMlPrivileges } from './get_privileges'; -import { ACCESS_DENIED_PATH } from '../management/management_urls'; - -let privileges: Privileges = getDefaultPrivileges(); -// manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53 -export function checkGetManagementMlJobs() { - return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => { - getManageMlPrivileges().then( - ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { - privileges = capabilities; - // Loop through all privileges to ensure they are all set to true. - const isManageML = Object.values(privileges).every(p => p === true); - - if (isManageML === true && isPlatinumOrTrialLicense === true) { - return resolve({ mlFeatureEnabledInSpace }); - } else { - window.location.href = ACCESS_DENIED_PATH; - return reject(); - } - } - ); - }); -} - -export function checkGetJobsPrivilege(): Promise { - return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; - // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. - // all other functionality is controlled by the return privileges object. - // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, - // allow the promise to resolve as the separate license check will redirect then user to - // a basic feature - if (privileges.canGetJobs || isPlatinumOrTrialLicense === false) { - return resolve(privileges); - } else { - window.location.href = '#/access-denied'; - return reject(); - } - }); - }); -} - -export function checkCreateJobsPrivilege(): Promise { - return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; - // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, - // allow the promise to resolve as the separate license check will redirect then user to - // a basic feature - if (privileges.canCreateJob || isPlatinumOrTrialLicense === false) { - return resolve(privileges); - } else { - // if the user has no permission to create a job, - // redirect them back to the Transforms Management page - window.location.href = '#/jobs'; - return reject(); - } - }); - }); -} - -export function checkFindFileStructurePrivilege(): Promise { - return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities }) => { - privileges = capabilities; - // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. - // all other functionality is controlled by the return privileges object - if (privileges.canFindFileStructure) { - return resolve(privileges); - } else { - window.location.href = '#/access-denied'; - return reject(); - } - }); - }); -} - -// check the privilege type and the license to see whether a user has permission to access a feature. -// takes the name of the privilege variable as specified in get_privileges.js -export function checkPermission(privilegeType: keyof Privileges) { - const licenseHasExpired = hasLicenseExpired(); - return privileges[privilegeType] === true && licenseHasExpired !== true; -} - -// create the text for the button's tooltips if the user's license has -// expired or if they don't have the privilege to press that button -export function createPermissionFailureMessage(privilegeType: keyof Privileges) { - let message = ''; - const licenseHasExpired = hasLicenseExpired(); - if (licenseHasExpired) { - message = i18n.translate('xpack.ml.privilege.licenseHasExpiredTooltip', { - defaultMessage: 'Your license has expired.', - }); - } else if (privilegeType === 'canCreateJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.createMLJobsTooltip', { - defaultMessage: 'You do not have permission to create Machine Learning jobs.', - }); - } else if (privilegeType === 'canStartStopDatafeed') { - message = i18n.translate('xpack.ml.privilege.noPermission.startOrStopDatafeedsTooltip', { - defaultMessage: 'You do not have permission to start or stop datafeeds.', - }); - } else if (privilegeType === 'canUpdateJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.editJobsTooltip', { - defaultMessage: 'You do not have permission to edit jobs.', - }); - } else if (privilegeType === 'canDeleteJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.deleteJobsTooltip', { - defaultMessage: 'You do not have permission to delete jobs.', - }); - } else if (privilegeType === 'canCreateCalendar') { - message = i18n.translate('xpack.ml.privilege.noPermission.createCalendarsTooltip', { - defaultMessage: 'You do not have permission to create calendars.', - }); - } else if (privilegeType === 'canDeleteCalendar') { - message = i18n.translate('xpack.ml.privilege.noPermission.deleteCalendarsTooltip', { - defaultMessage: 'You do not have permission to delete calendars.', - }); - } else if (privilegeType === 'canForecastJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.runForecastsTooltip', { - defaultMessage: 'You do not have permission to run forecasts.', - }); - } - return i18n.translate('xpack.ml.privilege.pleaseContactAdministratorTooltip', { - defaultMessage: '{message} Please contact your administrator.', - values: { - message, - }, - }); -} diff --git a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts b/x-pack/plugins/ml/public/application/privilege/get_privileges.ts deleted file mode 100644 index a3811779333d9..0000000000000 --- a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts +++ /dev/null @@ -1,40 +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 { ml } from '../services/ml_api_service'; - -import { setUpgradeInProgress } from '../services/upgrade_service'; -import { PrivilegesResponse } from '../../../common/types/privileges'; - -export function getPrivileges(): Promise { - return new Promise((resolve, reject) => { - ml.checkMlPrivileges() - .then((resp: PrivilegesResponse) => { - if (resp.upgradeInProgress === true) { - setUpgradeInProgress(true); - } - resolve(resp); - }) - .catch(() => { - reject(); - }); - }); -} - -export function getManageMlPrivileges(): Promise { - return new Promise((resolve, reject) => { - ml.checkManageMLPrivileges() - .then((resp: PrivilegesResponse) => { - if (resp.upgradeInProgress === true) { - setUpgradeInProgress(true); - } - resolve(resp); - }) - .catch(() => { - reject(); - }); - }); -} diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts index 776f0727389dd..958221df8a636 100644 --- a/x-pack/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts @@ -6,7 +6,7 @@ import { loadIndexPatterns, loadSavedSearches } from '../util/index_utils'; import { checkFullLicense } from '../license'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../capabilities/check_capabilities'; import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; @@ -28,6 +28,6 @@ export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Re getMlNodeCount, loadMlServerInfo, loadIndexPatterns: () => loadIndexPatterns(indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, loadSavedSearches, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index d257a9c080c35..fc2d517b2edb1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -16,7 +16,7 @@ import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; @@ -30,7 +30,7 @@ export const selectorRoute: MlRoute = { const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 174b3e3b4b338..1115a38870821 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -17,7 +17,7 @@ import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -43,7 +43,7 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index a3dbc9f97124c..1ec73fced82fe 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -12,7 +12,7 @@ import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; import { checkBasicLicense } from '../../../license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -39,7 +39,7 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index 9411b415e4e4d..b630b09b1a46d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -13,7 +13,7 @@ import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/inde import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; enum MODE { @@ -71,7 +71,7 @@ const PageWrapper: FC = ({ nextStepPath, deps, mode }) = const dataVizResolvers = { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index b1256e21888d9..1c91d7e94b241 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -15,7 +15,7 @@ import { Page } from '../../../jobs/new_job/pages/new_job'; import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; -import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { @@ -115,7 +115,7 @@ const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), - privileges: checkCreateJobsPrivilege, + privileges: checkCreateJobsCapabilitiesResolver, jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index ccb99985cb70c..9b08bbf35c448 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -13,7 +13,7 @@ import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; import { checkFullLicense } from '../../license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; @@ -38,7 +38,7 @@ export const overviewRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, loadMlServerInfo, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index 9d5c4e9c0b0a0..e015a3292acc4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -42,7 +45,7 @@ export const calendarListRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index bf039e3bd2354..ebd58120853a9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 6839ad833cb06..25bded1a52db1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; @@ -43,7 +46,7 @@ export const filterListRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 7b8bd6c3c81ac..2f4ccecf2f1a2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index 10ccc0987fe5d..7cb943c091c4e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -16,7 +16,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { Settings } from '../../../settings'; import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; @@ -32,7 +35,7 @@ export const settingsRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); 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 e160126833801..6e3fd08e90e38 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 @@ -15,7 +15,7 @@ import { jobs } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info'; -import { PrivilegesResponse } from '../../../../common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { Job, @@ -300,18 +300,17 @@ export const ml = { }); }, - checkMlPrivileges() { - return http({ + checkMlCapabilities() { + return http({ path: `${basePath()}/ml_capabilities`, method: 'GET', }); }, - checkManageMLPrivileges() { - return http({ + checkManageMLCapabilities() { + return http({ path: `${basePath()}/ml_capabilities`, method: 'GET', - query: { ignoreSpaces: true }, }); }, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 7e2d6814c0b23..7f5ade64e7f14 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -7,15 +7,15 @@ jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index 8750927ac1ee7..b2fce2c1474cb 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -13,15 +13,15 @@ import { CalendarsList } from './calendars_list'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js index 8b1ab853676cf..0266bc2a55318 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js @@ -8,7 +8,7 @@ // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); jest.mock('../../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index dbc815b5fc099..c1bcee4acdd37 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -12,7 +12,7 @@ import { FilterLists } from './filter_lists'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js index a5c1c872bfa5a..29b1185ddd4ab 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js @@ -6,7 +6,7 @@ // Create a mock for the privilege check used within the table to // enable/disable the 'New Filter' button. -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index c04ac40b962d5..7dd06268f7f8d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -32,7 +32,7 @@ import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { checkPermission, createPermissionFailureMessage, -} from '../../../privilege/check_privilege'; +} from '../../../capabilities/check_capabilities'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts new file mode 100644 index 0000000000000..8e350b8382276 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts @@ -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. + */ + +import { + adminMlCapabilities, + userMlCapabilities, + MlCapabilities, + getDefaultCapabilities, +} from '../../../../common/types/capabilities'; + +export function getAdminCapabilities() { + const caps: any = {}; + Object.keys(adminMlCapabilities).forEach(k => { + caps[k] = true; + }); + return { ...getUserCapabilities(), ...caps } as MlCapabilities; +} + +export function getUserCapabilities() { + const caps: any = {}; + Object.keys(userMlCapabilities).forEach(k => { + caps[k] = true; + }); + + return { ...getDefaultCapabilities(), ...caps } as MlCapabilities; +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts new file mode 100644 index 0000000000000..aef22debf3642 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts @@ -0,0 +1,58 @@ +/* + * 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 } from 'lodash'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; +import { ILicense } from '../../../../licensing/common/types'; +import { isFullLicense, isMinimumLicense } from '../../../common/license'; +import { MlCapabilities, basicLicenseMlCapabilities } from '../../../common/types/capabilities'; + +export const setupCapabilitiesSwitcher = ( + coreSetup: CoreSetup, + license$: Observable, + logger: Logger +) => { + coreSetup.capabilities.registerSwitcher(getSwitcher(license$, logger)); +}; + +function getSwitcher(license$: Observable, logger: Logger): CapabilitiesSwitcher { + return async (request, capabilities) => { + const isAnonymousRequest = !request.route.options.authRequired; + + if (isAnonymousRequest) { + return capabilities; + } + + try { + const license = await license$.pipe(take(1)).toPromise(); + + // full license, leave capabilities as they were + if (isFullLicense(license)) { + return capabilities; + } + + const mlCaps = capabilities.ml as MlCapabilities; + const originalCapabilities = cloneDeep(mlCaps); + + // not full licence, switch off all capabilities + Object.keys(mlCaps).forEach(k => { + mlCaps[k as keyof MlCapabilities] = false; + }); + + // for a basic license, reapply the original capabilities for the basic license features + if (isMinimumLicense(license)) { + basicLicenseMlCapabilities.forEach(c => (mlCaps[c] = originalCapabilities[c])); + } + + return capabilities; + } catch (e) { + logger.debug(`Error updating capabilities for ML based on licensing: ${e}`); + return capabilities; + } + }; +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts new file mode 100644 index 0000000000000..5093801d2d184 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -0,0 +1,255 @@ +/* + * 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 { getAdminCapabilities, getUserCapabilities } from './__mocks__/ml_capabilities'; +import { capabilitiesProvider } from './check_capabilities'; +import { MlLicense } from '../../../common/license'; +import { getDefaultCapabilities } from '../../../common/types/capabilities'; + +const mlLicense = { + isSecurityEnabled: () => true, + isFullLicense: () => true, +} as MlLicense; + +const mlLicenseBasic = { + isSecurityEnabled: () => true, + isFullLicense: () => false, +} as MlLicense; + +const mlIsEnabled = async () => true; +const mlIsNotEnabled = async () => false; + +const callWithRequestNonUpgrade = async () => ({ upgrade_mode: false }); +const callWithRequestUpgrade = async () => ({ upgrade_mode: true }); + +describe('check_capabilities', () => { + describe('getCapabilities() - right number of capabilities', () => { + test('kibana capabilities count', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities } = await getCapabilities(); + const count = Object.keys(capabilities).length; + expect(count).toBe(22); + done(); + }); + }); + + describe('getCapabilities() with security', () => { + test('ml_user capabilities only', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(true); + expect(capabilities.canDeleteJob).toBe(true); + expect(capabilities.canOpenJob).toBe(true); + expect(capabilities.canCloseJob).toBe(true); + expect(capabilities.canForecastJob).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(true); + expect(capabilities.canUpdateJob).toBe(true); + expect(capabilities.canUpdateDatafeed).toBe(true); + expect(capabilities.canPreviewDatafeed).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(true); + expect(capabilities.canDeleteCalendar).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(true); + expect(capabilities.canDeleteFilter).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); + expect(capabilities.canCreateDataFrameAnalytics).toBe(true); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); + done(); + }); + + test('upgrade in progress with full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(true); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('upgrade in progress with partial capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(true); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicense, + mlIsNotEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(false); + expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + }); + + test('full capabilities, basic license, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicenseBasic, + mlIsNotEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(false); + expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts new file mode 100644 index 0000000000000..a2ad83c5522de --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.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 { IScopedClusterClient } from 'kibana/server'; +import { + MlCapabilities, + adminMlCapabilities, + MlCapabilitiesResponse, +} from '../../../common/types/capabilities'; +import { upgradeCheckProvider } from './upgrade'; +import { MlLicense } from '../../../common/license'; + +export function capabilitiesProvider( + callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], + capabilities: MlCapabilities, + mlLicense: MlLicense, + isMlEnabledInSpace: () => Promise +) { + const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); + async function getCapabilities(): Promise { + const upgradeInProgress = await isUpgradeInProgress(); + const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); + const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); + + if (upgradeInProgress === true) { + // if an upgrade is in progress, set all admin capabilities to false + disableAdminPrivileges(capabilities); + } + + return { + capabilities, + upgradeInProgress, + isPlatinumOrTrialLicense, + mlFeatureEnabledInSpace, + }; + } + return { getCapabilities }; +} + +function disableAdminPrivileges(capabilities: MlCapabilities) { + Object.keys(adminMlCapabilities).forEach(k => { + capabilities[k as keyof MlCapabilities] = false; + }); +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/index.ts b/x-pack/plugins/ml/server/lib/capabilities/index.ts new file mode 100644 index 0000000000000..b73c6b87f6839 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { capabilitiesProvider } from './check_capabilities'; +export { setupCapabilitiesSwitcher } from './capabilities_switcher'; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts similarity index 100% rename from x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts rename to x-pack/plugins/ml/server/lib/capabilities/upgrade.ts diff --git a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts b/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts deleted file mode 100644 index ef82003ec80d6..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts +++ /dev/null @@ -1,154 +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. - */ - -export function callWithRequestProvider(testType: string) { - switch (testType) { - case 'partialPrivileges': - return partialPrivileges; - case 'fullPrivileges': - return fullPrivileges; - case 'upgradeWithFullPrivileges': - return upgradeWithFullPrivileges; - case 'upgradeWithPartialPrivileges': - return upgradeWithPartialPrivileges; - - default: - return fullPrivileges; - } -} - -const fullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const partialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const upgradeWithFullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'elastic', - has_all_requested: true, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const upgradeWithPartialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const fullClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': true, - 'cluster:admin/xpack/ml/datafeeds/update': true, - 'cluster:admin/xpack/ml/job/forecast': true, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': true, - 'cluster:admin/xpack/ml/datafeeds/preview': true, - 'cluster:admin/xpack/ml/datafeeds/start': true, - 'cluster:admin/xpack/ml/filters/put': true, - 'cluster:admin/xpack/ml/datafeeds/stop': true, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': true, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': true, - 'cluster:admin/xpack/ml/calendars/events/post': true, - 'cluster:admin/xpack/ml/job/close': true, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': true, - 'cluster:admin/xpack/ml/calendars/put': true, - 'cluster:admin/xpack/ml/calendars/events/delete': true, - 'cluster:admin/xpack/ml/datafeeds/put': true, - 'cluster:admin/xpack/ml/job/open': true, - 'cluster:admin/xpack/ml/job/delete': true, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': true, - 'cluster:admin/xpack/ml/job/update': true, - 'cluster:admin/xpack/ml/calendars/delete': true, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; - -// the same as ml_user role -const partialClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': false, - 'cluster:admin/xpack/ml/datafeeds/update': false, - 'cluster:admin/xpack/ml/job/forecast': false, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': false, - 'cluster:admin/xpack/ml/datafeeds/preview': false, - 'cluster:admin/xpack/ml/datafeeds/start': false, - 'cluster:admin/xpack/ml/filters/put': false, - 'cluster:admin/xpack/ml/datafeeds/stop': false, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': false, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': false, - 'cluster:admin/xpack/ml/calendars/events/post': false, - 'cluster:admin/xpack/ml/job/close': false, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': false, - 'cluster:admin/xpack/ml/calendars/put': false, - 'cluster:admin/xpack/ml/calendars/events/delete': false, - 'cluster:admin/xpack/ml/datafeeds/put': false, - 'cluster:admin/xpack/ml/job/open': false, - 'cluster:admin/xpack/ml/job/delete': false, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': false, - 'cluster:admin/xpack/ml/job/update': false, - 'cluster:admin/xpack/ml/calendars/delete': false, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts deleted file mode 100644 index d8435e9026250..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts +++ /dev/null @@ -1,515 +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 { callWithRequestProvider } from './__mocks__/call_with_request'; -import { privilegesProvider } from './check_privileges'; -import { mlPrivileges } from './privileges'; -import { MlLicense } from '../../../common/license'; - -const mlLicenseWithSecurity = { - isSecurityEnabled: () => true, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurity = { - isSecurityEnabled: () => false, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurityBasicLicense = { - isSecurityEnabled: () => false, - isFullLicense: () => false, -} as MlLicense; - -const mlLicenseWithSecurityBasicLicense = { - isSecurityEnabled: () => true, - isFullLicense: () => false, -} as MlLicense; - -const mlIsEnabled = async () => true; -const mlIsNotEnabled = async () => false; - -describe('check_privileges', () => { - describe('getPrivileges() - right number of capabilities', () => { - test('es capabilities count', async done => { - const count = mlPrivileges.cluster.length; - expect(count).toBe(27); - done(); - }); - - test('kibana capabilities count', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities } = await getPrivileges(); - const count = Object.keys(capabilities).length; - expect(count).toBe(22); - done(); - }); - }); - - describe('getPrivileges() with security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full capabilities', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(true); - expect(capabilities.canDeleteJob).toBe(true); - expect(capabilities.canOpenJob).toBe(true); - expect(capabilities.canCloseJob).toBe(true); - expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(true); - expect(capabilities.canUpdateJob).toBe(true); - expect(capabilities.canUpdateDatafeed).toBe(true); - expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(true); - expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(true); - expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); - expect(capabilities.canCreateDataFrameAnalytics).toBe(true); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full capabilities, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - }); - - describe('getPrivileges() without security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(true); - expect(capabilities.canDeleteJob).toBe(true); - expect(capabilities.canOpenJob).toBe(true); - expect(capabilities.canCloseJob).toBe(true); - expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(true); - expect(capabilities.canUpdateJob).toBe(true); - expect(capabilities.canUpdateDatafeed).toBe(true); - expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(true); - expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(true); - expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); - expect(capabilities.canCreateDataFrameAnalytics).toBe(true); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities only, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts deleted file mode 100644 index df61ad0111a03..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ /dev/null @@ -1,269 +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 { IScopedClusterClient } from 'kibana/server'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { upgradeCheckProvider } from './upgrade'; -import { MlLicense } from '../../../common/license'; - -import { mlPrivileges } from './privileges'; - -type ClusterPrivilege = Record; - -export interface MlCapabilities { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} - -export function privilegesProvider( - callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], - mlLicense: MlLicense, - isMlEnabledInSpace: () => Promise, - ignoreSpaces: boolean = false -) { - const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); - async function getPrivileges(): Promise { - // get the default privileges, forced to be false. - const privileges = getDefaultPrivileges(); - - const upgradeInProgress = await isUpgradeInProgress(); - const isSecurityEnabled = mlLicense.isSecurityEnabled(); - - const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); - const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); - - const setGettingPrivileges = isPlatinumOrTrialLicense - ? setFullGettingPrivileges - : setBasicGettingPrivileges; - - const setActionPrivileges = isPlatinumOrTrialLicense - ? setFullActionPrivileges - : setBasicActionPrivileges; - - if (mlFeatureEnabledInSpace === false && ignoreSpaces === false) { - // if ML isn't enabled in the current space, - // return with the default privileges (all false) - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - - if (isSecurityEnabled === false) { - if (upgradeInProgress === true) { - // if security is disabled and an upgrade in is progress, - // force all "getting" privileges to be true - // leaving all "setting" privileges to be the default false - setGettingPrivileges({}, privileges, true); - } else { - // if no upgrade is in progress, - // get all privileges forced to true - setGettingPrivileges({}, privileges, true); - setActionPrivileges({}, privileges, true); - } - } else { - // security enabled - // load all ml privileges for this user. - const { cluster } = await callAsCurrentUser('ml.privilegeCheck', { body: mlPrivileges }); - setGettingPrivileges(cluster, privileges); - if (upgradeInProgress === false) { - // if an upgrade is in progress, don't apply the "setting" - // privileges. leave them to be the default false. - setActionPrivileges(cluster, privileges); - } - } - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - return { getPrivileges }; -} - -function setFullGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetJobs = true; - } - - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/datafeeds/get'] && - cluster['cluster:monitor/xpack/ml/datafeeds/stats/get']) - ) { - privileges.canGetDatafeeds = true; - } - - // Calendars - if (forceTrue || cluster['cluster:monitor/xpack/ml/calendars/get']) { - privileges.canGetCalendars = true; - } - - // Filters - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/get']) { - privileges.canGetFilters = true; - } - - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetDataFrameAnalytics = true; - } -} - -function setFullActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/update']) { - privileges.canUpdateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/open']) { - privileges.canOpenJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/close']) { - privileges.canCloseJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/forecast']) { - privileges.canForecastJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/update']) { - privileges.canUpdateDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/preview']) { - privileges.canPreviewDatafeed = true; - } - - // Calendars - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/put'] && - cluster['cluster:admin/xpack/ml/calendars/jobs/update'] && - cluster['cluster:admin/xpack/ml/calendars/events/post']) - ) { - privileges.canCreateCalendar = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/delete'] && - cluster['cluster:admin/xpack/ml/calendars/events/delete']) - ) { - privileges.canDeleteCalendar = true; - } - - // Filters - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/filters/put'] && - cluster['cluster:admin/xpack/ml/filters/update']) - ) { - privileges.canCreateFilter = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/delete']) { - privileges.canDeleteFilter = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDataFrameAnalytics = true; - } -} - -function setBasicGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } -} - -function setBasicActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) {} diff --git a/x-pack/plugins/ml/server/lib/check_privileges/index.ts b/x-pack/plugins/ml/server/lib/check_privileges/index.ts deleted file mode 100644 index 67b435116aa00..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { privilegesProvider, MlCapabilities } from './check_privileges'; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts deleted file mode 100644 index 9fcd28dd68105..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts +++ /dev/null @@ -1,37 +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. - */ - -export const mlPrivileges = { - cluster: [ - 'cluster:monitor/xpack/ml/job/get', - 'cluster:monitor/xpack/ml/job/stats/get', - 'cluster:monitor/xpack/ml/datafeeds/get', - 'cluster:monitor/xpack/ml/datafeeds/stats/get', - 'cluster:monitor/xpack/ml/calendars/get', - 'cluster:admin/xpack/ml/job/put', - 'cluster:admin/xpack/ml/job/delete', - 'cluster:admin/xpack/ml/job/update', - 'cluster:admin/xpack/ml/job/open', - 'cluster:admin/xpack/ml/job/close', - 'cluster:admin/xpack/ml/job/forecast', - 'cluster:admin/xpack/ml/datafeeds/put', - 'cluster:admin/xpack/ml/datafeeds/delete', - 'cluster:admin/xpack/ml/datafeeds/start', - 'cluster:admin/xpack/ml/datafeeds/stop', - 'cluster:admin/xpack/ml/datafeeds/update', - 'cluster:admin/xpack/ml/datafeeds/preview', - 'cluster:admin/xpack/ml/calendars/put', - 'cluster:admin/xpack/ml/calendars/delete', - 'cluster:admin/xpack/ml/calendars/jobs/update', - 'cluster:admin/xpack/ml/calendars/events/post', - 'cluster:admin/xpack/ml/calendars/events/delete', - 'cluster:admin/xpack/ml/filters/put', - 'cluster:admin/xpack/ml/filters/get', - 'cluster:admin/xpack/ml/filters/update', - 'cluster:admin/xpack/ml/filters/delete', - 'cluster:monitor/xpack/ml/findfilestructure', - ], -}; diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index 64d750f511f3a..581770e59043f 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,6 @@ */ import { difference } from 'lodash'; -import Boom from 'boom'; import { IScopedClusterClient } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; @@ -33,43 +32,31 @@ export class CalendarManager { } async getCalendar(calendarId: string) { - try { - const resp = await this._client('ml.calendars', { - calendarId, - }); - - const calendars = resp.calendars; - if (calendars.length) { - const calendar = calendars[0]; - calendar.events = await this._eventManager.getCalendarEvents(calendarId); - return calendar; - } else { - throw Boom.notFound(`Calendar with the id "${calendarId}" not found`); - } - } catch (error) { - throw Boom.badRequest(error); - } + const resp = await this._client('ml.calendars', { + calendarId, + }); + + const calendars = resp.calendars; + const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found. + calendar.events = await this._eventManager.getCalendarEvents(calendarId); + return calendar; } async getAllCalendars() { - try { - const calendarsResp = await this._client('ml.calendars'); - - const events: CalendarEvent[] = await this._eventManager.getAllEvents(); - const calendars: Calendar[] = calendarsResp.calendars; - calendars.forEach(cal => (cal.events = [])); - - // loop events and combine with related calendars - events.forEach(event => { - const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); - if (calendar) { - calendar.events.push(event); - } - }); - return calendars; - } catch (error) { - throw Boom.badRequest(error); - } + const calendarsResp = await this._client('ml.calendars'); + + const events: CalendarEvent[] = await this._eventManager.getAllEvents(); + const calendars: Calendar[] = calendarsResp.calendars; + calendars.forEach(cal => (cal.events = [])); + + // loop events and combine with related calendars + events.forEach(event => { + const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); + if (calendar) { + calendar.events.push(event); + } + }); + return calendars; } /** @@ -78,12 +65,8 @@ export class CalendarManager { * @returns {Promise<*>} */ async getCalendarsByIds(calendarIds: string) { - try { - const calendars: Calendar[] = await this.getAllCalendars(); - return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); - } catch (error) { - throw Boom.badRequest(error); - } + const calendars: Calendar[] = await this.getAllCalendars(); + return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); } async newCalendar(calendar: FormCalendar) { @@ -91,75 +74,67 @@ export class CalendarManager { const events = calendar.events; delete calendar.calendarId; delete calendar.events; - try { - await this._client('ml.addCalendar', { - calendarId, - body: calendar, - }); - - if (events.length) { - await this._eventManager.addEvents(calendarId, events); - } + await this._client('ml.addCalendar', { + calendarId, + body: calendar, + }); - // return the newly created calendar - return await this.getCalendar(calendarId); - } catch (error) { - throw Boom.badRequest(error); + if (events.length) { + await this._eventManager.addEvents(calendarId, events); } + + // return the newly created calendar + return await this.getCalendar(calendarId); } async updateCalendar(calendarId: string, calendar: Calendar) { const origCalendar: Calendar = await this.getCalendar(calendarId); - try { - // update job_ids - const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); - const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); - - // workout the differences between the original events list and the new one - // if an event has no event_id, it must be new - const eventsToAdd = calendar.events.filter( - event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // if an event in the original calendar cannot be found, it must have been deleted - const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( - event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // note, both of the loops below could be removed if the add and delete endpoints - // allowed multiple job_ids - - // add all new jobs - if (jobsToAdd.length) { - await this._client('ml.addJobToCalendar', { - calendarId, - jobId: jobsToAdd.join(','), - }); - } - - // remove all removed jobs - if (jobsToRemove.length) { - await this._client('ml.removeJobFromCalendar', { - calendarId, - jobId: jobsToRemove.join(','), - }); - } + // update job_ids + const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); + const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); + + // workout the differences between the original events list and the new one + // if an event has no event_id, it must be new + const eventsToAdd = calendar.events.filter( + event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // if an event in the original calendar cannot be found, it must have been deleted + const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( + event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // note, both of the loops below could be removed if the add and delete endpoints + // allowed multiple job_ids + + // add all new jobs + if (jobsToAdd.length) { + await this._client('ml.addJobToCalendar', { + calendarId, + jobId: jobsToAdd.join(','), + }); + } - // add all new events - if (eventsToAdd.length !== 0) { - await this._eventManager.addEvents(calendarId, eventsToAdd); - } + // remove all removed jobs + if (jobsToRemove.length) { + await this._client('ml.removeJobFromCalendar', { + calendarId, + jobId: jobsToRemove.join(','), + }); + } - // remove all removed events - await Promise.all( - eventsToRemove.map(async event => { - await this._eventManager.deleteEvent(calendarId, event.event_id); - }) - ); - } catch (error) { - throw Boom.badRequest(error); + // add all new events + if (eventsToAdd.length !== 0) { + await this._eventManager.addEvents(calendarId, eventsToAdd); } + // remove all removed events + await Promise.all( + eventsToRemove.map(async event => { + await this._eventManager.deleteEvent(calendarId, event.event_id); + }) + ); + // return the updated calendar return await this.getCalendar(calendarId); } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index 488839f68b3fe..41240e2695f6f 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -23,41 +21,29 @@ export class EventManager { } async getCalendarEvents(calendarId: string) { - try { - const resp = await this._client('ml.events', { calendarId }); + const resp = await this._client('ml.events', { calendarId }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - try { - const resp = await this._client('ml.events', { - calendarId, - jobId, - }); + const resp = await this._client('ml.events', { + calendarId, + jobId, + }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - try { - return await this._client('ml.addEvent', { - calendarId, - body, - }); - } catch (error) { - throw Boom.badRequest(error); - } + return await this._client('ml.addEvent', { + calendarId, + body, + }); } async deleteEvent(calendarId: string, eventId: string) { diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index c7add12be142c..67e80a3bc44c0 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -7,14 +7,18 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, + CoreStart, Plugin, IScopedClusterClient, + KibanaRequest, Logger, PluginInitializerContext, ICustomClusterClient, + CapabilitiesStart, } from 'kibana/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; +import { MlCapabilities } from '../common/types/capabilities'; import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; import { initMlTelemetry } from './lib/telemetry'; @@ -41,6 +45,8 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; +import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities'; +import { setupCapabilitiesSwitcher } from './lib/capabilities'; declare module 'kibana/server' { interface RequestHandlerContext { @@ -59,6 +65,7 @@ export class MlServerPlugin implements Plugin initSampleDataSets(mlLicense, plugins), ]); + // initialize capabilities switcher to add license filter to ml capabilities + setupCapabilitiesSwitcher(coreSetup, plugins.licensing.license$, this.log); + // Can access via router's handler function 'context' parameter - context.ml.mlClient const mlClient = coreSetup.elasticsearch.createClient(PLUGIN_ID, { plugins: [elasticsearchJsPlugin], @@ -132,6 +145,14 @@ export class MlServerPlugin implements Plugin { + if (this.capabilities === null) { + return null; + } + const capabilities = await this.capabilities.resolveCapabilities(request); + return capabilities.ml as MlCapabilities; + }; + annotationRoutes(routeInit, plugins.security); calendars(routeInit); dataFeedRoutes(routeInit); @@ -144,24 +165,27 @@ export class MlServerPlugin implements Plugin { try { - const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; // if spaces is disabled force isMlEnabledInSpace to be true const { isMlEnabledInSpace } = spaces !== undefined ? spacesUtilsProvider(spaces, (request as unknown) as Request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + return response.customError(wrapError(new Error('resolveMlCapabilities is not defined'))); + } + + const { getCapabilities } = capabilitiesProvider( context.ml!.mlClient.callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); return response.ok({ - body: await getPrivileges(), + body: await getCapabilities(), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index cedf18e80906f..698ac8e6261e5 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; +import { APICaller, KibanaRequest } from 'kibana/server'; import { SearchResponse, SearchParams } from 'elasticsearch'; import { MlServerLicense } from '../../lib/license'; import { CloudSetup } from '../../../../cloud/server'; import { LicenseCheck } from '../license_checks'; -import { spacesUtilsProvider, RequestFacade } from '../../lib/spaces_utils'; +import { spacesUtilsProvider } from '../../lib/spaces_utils'; import { SpacesPluginSetup } from '../../../../spaces/server'; -import { privilegesProvider, MlCapabilities } from '../../lib/check_privileges'; +import { capabilitiesProvider } from '../../lib/capabilities'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities'; export interface MlSystemProvider { mlSystemProvider( callAsCurrentUser: APICaller, - request: RequestFacade + request: KibanaRequest ): { - mlCapabilities(ignoreSpaces?: boolean): Promise; + mlCapabilities(): Promise; mlInfo(): Promise; mlSearch(searchParams: SearchParams): Promise>; }; @@ -31,12 +32,13 @@ export function getMlSystemProvider( isFullLicense: LicenseCheck, mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup | undefined + cloud: CloudSetup | undefined, + resolveMlCapabilities: ResolveMlCapabilities ): MlSystemProvider { return { - mlSystemProvider(callAsCurrentUser: APICaller, request: RequestFacade) { + mlSystemProvider(callAsCurrentUser: APICaller, request: KibanaRequest) { return { - mlCapabilities(ignoreSpaces?: boolean) { + async mlCapabilities() { isMinimumLicense(); const { isMlEnabledInSpace } = @@ -44,13 +46,18 @@ export function getMlSystemProvider( ? spacesUtilsProvider(spaces, request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + throw new Error('resolveMlCapabilities is not defined'); + } + + const { getCapabilities } = capabilitiesProvider( callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); - return getPrivileges(); + return getCapabilities(); }, async mlInfo(): Promise { isMinimumLicense(); diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index f08eb5c23b272..f2d20a72444be 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -17,6 +17,7 @@ import { AnomalyDetectorsProvider, getAnomalyDetectorsProvider, } from './providers/anomaly_detectors'; +import { ResolveMlCapabilities } from '../../common/types/capabilities'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -27,14 +28,22 @@ export type SharedServices = JobServiceProvider & export function createSharedServices( mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup + cloud: CloudSetup, + resolveMlCapabilities: ResolveMlCapabilities ): SharedServices { const { isFullLicense, isMinimumLicense } = licenseChecks(mlLicense); return { ...getJobServiceProvider(isFullLicense), ...getAnomalyDetectorsProvider(isFullLicense), - ...getMlSystemProvider(isMinimumLicense, isFullLicense, mlLicense, spaces, cloud), + ...getMlSystemProvider( + isMinimumLicense, + isFullLicense, + mlLicense, + spaces, + cloud, + resolveMlCapabilities + ), ...getModulesProvider(isFullLicense), ...getResultsServiceProvider(isFullLicense), }; diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index ff4d07bd79e42..d4cd61a7fa4f7 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -13,6 +13,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { MlServerLicense } from './lib/license'; +import { ResolveMlCapabilities } from '../common/types/capabilities'; export interface LicenseCheckResult { isAvailable: boolean; @@ -26,6 +27,11 @@ export interface LicenseCheckResult { export interface SystemRouteDeps { cloud: CloudSetup; spaces?: SpacesPluginSetup; + resolveMlCapabilities: ResolveMlCapabilities; +} + +export interface JobServiceRouteDeps { + resolveMlCapabilities: ResolveMlCapabilities; } export interface PluginsSetup { diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index dfe8ab31f972c..3a3ec6ac799d2 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -16,8 +16,33 @@ import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../common/constants'; * major version! * @return {Array} array of rename operations and callback function for rename logging */ -export const deprecations = ({ rename }: ConfigDeprecationFactory): ConfigDeprecation[] => { +export const deprecations = ({ + rename, + renameFromRoot, +}: ConfigDeprecationFactory): ConfigDeprecation[] => { return [ + // This order matters. The "blanket rename" needs to happen at the end + renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), + renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), + renameFromRoot( + 'xpack.monitoring.show_license_expiration', + 'monitoring.ui.show_license_expiration' + ), + renameFromRoot( + 'xpack.monitoring.ui.container.elasticsearch.enabled', + 'monitoring.ui.container.elasticsearch.enabled' + ), + renameFromRoot( + 'xpack.monitoring.ui.container.logstash.enabled', + 'monitoring.ui.container.logstash.enabled' + ), + renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch'), + renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), + renameFromRoot( + 'xpack.monitoring.elasticsearch.logFetchCount', + 'monitoring.ui.elasticsearch.logFetchCount' + ), + renameFromRoot('xpack.monitoring', 'monitoring'), (config, fromPath, logger) => { const clusterAlertsEnabled = get(config, 'cluster_alerts.enabled'); const emailNotificationsEnabled = diff --git a/x-pack/plugins/remote_clusters/public/index.ts b/x-pack/plugins/remote_clusters/public/index.ts index 6ba021b157c3e..127ec2a670645 100644 --- a/x-pack/plugins/remote_clusters/public/index.ts +++ b/x-pack/plugins/remote_clusters/public/index.ts @@ -3,8 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { PluginInitializerContext } from 'kibana/public'; import { RemoteClustersUIPlugin } from './plugin'; +export { RemoteClustersPluginSetup } from './plugin'; + export const plugin = (initializerContext: PluginInitializerContext) => new RemoteClustersUIPlugin(initializerContext); diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index d110c461c1e3f..22f98e94748d8 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -14,7 +14,12 @@ import { init as initNotification } from './application/services/notification'; import { init as initRedirect } from './application/services/redirect'; import { Dependencies, ClientConfigType } from './types'; -export class RemoteClustersUIPlugin implements Plugin { +export interface RemoteClustersPluginSetup { + isUiEnabled: boolean; +} + +export class RemoteClustersUIPlugin + implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} setup( @@ -55,6 +60,10 @@ export class RemoteClustersUIPlugin implements Plugin new RemoteClustersServerPlugin(ctx); diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index fca4a5dbc5f94..a7ca30a6bf96d 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -18,19 +19,26 @@ import { registerDeleteRoute, } from './routes/api'; -export class RemoteClustersServerPlugin implements Plugin { +export interface RemoteClustersPluginSetup { + isUiEnabled: boolean; +} + +export class RemoteClustersServerPlugin + implements Plugin { licenseStatus: LicenseStatus; log: Logger; - config: Observable; + config$: Observable; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config = config.create(); + this.config$ = config.create(); this.licenseStatus = { valid: false }; } async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) { const router = http.createRouter(); + const config = await this.config$.pipe(first()).toPromise(); + const routeDependencies: RouteDependencies = { router, getLicenseStatus: () => this.licenseStatus, @@ -64,6 +72,10 @@ export class RemoteClustersServerPlugin implements Plugin } } }); + + return { + isUiEnabled: config.ui.enabled, + }; } start() {} diff --git a/x-pack/plugins/reporting/common/types.d.ts b/x-pack/plugins/reporting/common/types.d.ts new file mode 100644 index 0000000000000..34f0bc9ac8a36 --- /dev/null +++ b/x-pack/plugins/reporting/common/types.d.ts @@ -0,0 +1,7 @@ +/* + * 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 { ConfigType } from '../server/config'; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 5756d29face12..8079c5b1d9887 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -7,13 +7,6 @@ export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; -export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { - jobCompletionNotifier: { - interval: 10000, - intervalErrorMultiplier: 5, - }, -}; - // Routes export const API_BASE_URL = '/api/reporting'; export const API_LIST_URL = `${API_BASE_URL}/jobs`; diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 26d661e29bd94..77faf837e6505 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -4,15 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CoreSetup, - CoreStart, - HttpSetup, - Plugin, - PluginInitializerContext, - NotificationsStart, -} from '../../../src/core/public'; - export type JobId = string; export type JobStatus = | 'completed' @@ -21,9 +12,6 @@ export type JobStatus = | 'processing' | 'failed'; -export type HttpService = HttpSetup; -export type NotificationsService = NotificationsStart; - export interface SourceJob { _id: JobId; _source: { diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap index 271d61c224210..b05e74c516cd4 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap @@ -219,11 +219,14 @@ Array [
- - Report - + + + Report + +
@@ -246,11 +249,14 @@ Array [
- - Created at - + + + Created at + +
@@ -273,11 +279,14 @@ Array [
- - Status - + + + Status + +
@@ -298,11 +307,14 @@ Array [
- - Actions - + + + Actions + +
@@ -514,11 +526,14 @@ Array [
- - Report - + + + Report + +
@@ -541,11 +556,14 @@ Array [
- - Created at - + + + Created at + +
@@ -568,11 +586,14 @@ Array [
- - Status - + + + Status + +
@@ -593,11 +614,14 @@ Array [
- - Actions - + + + Actions + +
diff --git a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx index 3f7c210eb3696..e8916ff82e121 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx @@ -83,7 +83,12 @@ export class ReportDeleteButton extends PureComponent { return ( - this.showConfirm()} iconType="trash" color={'danger'}> + this.showConfirm()} + iconType="trash" + color={'danger'} + data-test-subj="deleteReportButton" + > {intl.formatMessage( { id: 'xpack.reporting.listing.table.deleteReportButton', diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx index 380a3b3295b9f..787279e6caf9b 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -47,12 +47,24 @@ const toasts = { addDanger: jest.fn(), } as any; +const mockPollConfig = { + jobCompletionNotifier: { + interval: 5000, + intervalErrorMultiplier: 3, + }, + jobsRefresh: { + interval: 5000, + intervalErrorMultiplier: 3, + }, +}; + describe('ReportListing', () => { it('Report job listing with some items', () => { const wrapper = mountWithIntl( @@ -74,6 +86,7 @@ describe('ReportListing', () => { } + pollConfig={mockPollConfig} redirect={jest.fn()} toasts={toasts} /> diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 885e9577471a0..d8f9b7d37cfbf 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import moment from 'moment'; -import { Component, Fragment, default as React } from 'react'; +import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { Poller } from '../../common/poller'; -import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; +import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; +import { ClientConfigType } from '../plugin'; import { ReportDeleteButton, ReportDownloadButton, @@ -53,6 +54,7 @@ export interface Props { intl: InjectedIntl; apiClient: ReportingAPIClient; license$: LicensingPluginSetup['license$']; + pollConfig: ClientConfigType['poll']; redirect: ApplicationStart['navigateToApp']; toasts: ToastsSetup; } @@ -167,12 +169,10 @@ class ReportListingUi extends Component { functionToPoll: () => { return this.fetchJobs(); }, - pollFrequencyInMillis: - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.interval, + pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval, trailing: false, continuePollingOnError: true, - pollFrequencyErrorMultiplier: - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.intervalErrorMultiplier, + pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier, }); this.poller.start(); this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 3c121f1712685..eed6d5dd141e7 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -4,30 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; +import { NotificationsSetup } from 'src/core/public'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, JOB_STATUS_WARNINGS, } from '../../constants'; - -import { - JobId, - JobSummary, - JobStatusBuckets, - NotificationsService, - SourceJob, -} from '../../index.d'; - +import { JobId, JobStatusBuckets, JobSummary, SourceJob } from '../../index.d'; import { - getSuccessToast, getFailureToast, + getGeneralErrorToast, + getSuccessToast, getWarningFormulasToast, getWarningMaxSizeToast, - getGeneralErrorToast, } from '../components'; import { ReportingAPIClient } from './reporting_api_client'; @@ -47,7 +40,7 @@ function summarizeJob(src: SourceJob): JobSummary { } export class ReportingNotifierStreamHandler { - constructor(private notifications: NotificationsService, private apiClient: ReportingAPIClient) {} + constructor(private notifications: NotificationsSetup, private apiClient: ReportingAPIClient) {} /* * Use Kibana Toast API to show our messages diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 08ba10ff69207..c40e7ad373eaf 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -4,44 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; import ReactDOM from 'react-dom'; +import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; +import { + CoreSetup, + CoreStart, + NotificationsSetup, + Plugin, + PluginInitializerContext, +} from 'src/core/public'; import { ManagementSetup } from 'src/plugins/management/public'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import { I18nProvider } from '@kbn/i18n/react'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; - -import { ReportListing } from './components/report_listing'; -import { getGeneralErrorToast } from './components'; - -import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; -import { ReportingAPIClient } from './lib/reporting_api_client'; -import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; -import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; -import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; - -import { LicensingPluginSetup } from '../../licensing/public'; +import { JobId, JobStatusBuckets } from '../'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; -import { SharePluginSetup } from '../../../../src/plugins/share/public'; - import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; +import { SharePluginSetup } from '../../../../src/plugins/share/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ConfigType } from '../common/types'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; +import { getGeneralErrorToast } from './components'; +import { ReportListing } from './components/report_listing'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; +import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; +import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; +import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; -import { - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG, - JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, -} from '../constants'; - -import { JobId, JobStatusBuckets, NotificationsService } from '..'; - -const { - jobCompletionNotifier: { interval: JOBS_REFRESH_INTERVAL }, -} = JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG; +export interface ClientConfigType { + poll: ConfigType['poll']; +} function getStored(): JobId[] { const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY); @@ -49,7 +47,7 @@ function getStored(): JobId[] { } function handleError( - notifications: NotificationsService, + notifications: NotificationsSetup, err: Error ): Rx.Observable { notifications.toasts.addDanger( @@ -64,18 +62,19 @@ function handleError( return Rx.of({ completed: [], failed: [] }); } -export class ReportingPublicPlugin implements Plugin { +export class ReportingPublicPlugin implements Plugin { + private config: ClientConfigType; private readonly stop$ = new Rx.ReplaySubject(1); - private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', }); - private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { defaultMessage: 'Reporting', }); - constructor(initializerContext: PluginInitializerContext) {} + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); + } public setup( core: CoreSetup, @@ -130,6 +129,7 @@ export class ReportingPublicPlugin implements Plugin { @@ -163,8 +163,9 @@ export class ReportingPublicPlugin implements Plugin { const { http, notifications } = core; const apiClient = new ReportingAPIClient(http); const streamHandler = new StreamHandler(notifications, apiClient); + const { interval } = this.config.poll.jobsRefresh; - Rx.timer(0, JOBS_REFRESH_INTERVAL) + Rx.timer(0, interval) .pipe( takeUntil(this.stop$), // stop the interval when stop method is called map(() => getStored()), // read all pending job IDs from session storage diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index f0a0a093aa8c0..a0d7618322c65 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -10,6 +10,7 @@ import { ConfigSchema, ConfigType } from './schema'; export { createConfig$ } from './create_config'; export const config: PluginConfigDescriptor = { + exposeToBrowser: { poll: true }, schema: ConfigSchema, deprecations: ({ unused }) => [ unused('capture.browser.chromium.maxScreenshotDimension'), diff --git a/x-pack/plugins/security/public/authentication/authentication_service.ts b/x-pack/plugins/security/public/authentication/authentication_service.ts index 979f7095cf933..2e73b8cd04482 100644 --- a/x-pack/plugins/security/public/authentication/authentication_service.ts +++ b/x-pack/plugins/security/public/authentication/authentication_service.ts @@ -25,6 +25,11 @@ export interface AuthenticationServiceSetup { * Returns currently authenticated user and throws if current user isn't authenticated. */ getCurrentUser: () => Promise; + + /** + * Determines if API Keys are currently enabled. + */ + areAPIKeysEnabled: () => Promise; } export class AuthenticationService { @@ -37,11 +42,15 @@ export class AuthenticationService { const getCurrentUser = async () => (await http.get('/internal/security/me', { asSystemRequest: true })) as AuthenticatedUser; + const areAPIKeysEnabled = async () => + ((await http.get('/internal/security/api_key/_enabled')) as { apiKeysEnabled: boolean }) + .apiKeysEnabled; + loginApp.create({ application, config, getStartServices, http }); logoutApp.create({ application, http }); loggedOutApp.create({ application, getStartServices, http }); overwrittenSessionApp.create({ application, authc: { getCurrentUser }, getStartServices }); - return { getCurrentUser }; + return { getCurrentUser, areAPIKeysEnabled }; } } diff --git a/x-pack/plugins/security/public/authentication/index.mock.ts b/x-pack/plugins/security/public/authentication/index.mock.ts index c8d77a5b62c6f..dee0a24ab27c2 100644 --- a/x-pack/plugins/security/public/authentication/index.mock.ts +++ b/x-pack/plugins/security/public/authentication/index.mock.ts @@ -9,5 +9,6 @@ import { AuthenticationServiceSetup } from './authentication_service'; export const authenticationMock = { createSetup: (): jest.Mocked => ({ getCurrentUser: jest.fn(), + areAPIKeysEnabled: jest.fn(), }), }; diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts index 8e0ee73dfb613..213c26d5287dc 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts @@ -10,7 +10,7 @@ import { AuthenticationServiceSetup } from '../authentication_service'; interface CreateDeps { application: ApplicationSetup; - authc: AuthenticationServiceSetup; + authc: Pick; getStartServices: StartServicesAccessor; } diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx index 1093957761d1c..5b77266068ebf 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx @@ -14,7 +14,7 @@ import { AuthenticationStatePage } from '../components'; interface Props { basePath: IBasePath; - authc: AuthenticationServiceSetup; + authc: Pick; } export function OverwrittenSessionPage({ authc, basePath }: Props) { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts index 372b1e56a73c4..a127379d97241 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -10,6 +10,7 @@ import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; isAdmin: boolean; + canManage: boolean; } interface InvalidateApiKeysResponse { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index ae6ef4aa0fc34..dea04a0eac396 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -18,7 +18,6 @@ import { APIKeysGridPage } from './api_keys_grid_page'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { apiKeysAPIClientMock } from '../index.mock'; -const mock403 = () => ({ body: { statusCode: 403 } }); const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( @@ -48,6 +47,7 @@ describe('APIKeysGridPage', () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, areApiKeysEnabled: true, + canManage: true, }); apiClientMock.getApiKeys.mockResolvedValue({ apiKeys: [ @@ -82,6 +82,7 @@ describe('APIKeysGridPage', () => { it('renders a callout when API keys are not enabled', async () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, + canManage: true, areApiKeysEnabled: false, }); @@ -95,7 +96,11 @@ describe('APIKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + apiClientMock.checkPrivileges.mockResolvedValue({ + canManage: false, + isAdmin: false, + areApiKeysEnabled: true, + }); const wrapper = mountWithIntl(); @@ -152,6 +157,7 @@ describe('APIKeysGridPage', () => { beforeEach(() => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: false, + canManage: true, areApiKeysEnabled: true, }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 698c0d37dbc64..9db09a34d3c3f 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -26,7 +26,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; -import _ from 'lodash'; import { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; @@ -47,10 +46,10 @@ interface State { isLoadingApp: boolean; isLoadingTable: boolean; isAdmin: boolean; + canManage: boolean; areApiKeysEnabled: boolean; apiKeys: ApiKey[]; selectedItems: ApiKey[]; - permissionDenied: boolean; error: any; } @@ -63,9 +62,9 @@ export class APIKeysGridPage extends Component { isLoadingApp: true, isLoadingTable: false, isAdmin: false, + canManage: false, areApiKeysEnabled: false, apiKeys: [], - permissionDenied: false, selectedItems: [], error: undefined, }; @@ -77,19 +76,15 @@ export class APIKeysGridPage extends Component { public render() { const { - permissionDenied, isLoadingApp, isLoadingTable, areApiKeysEnabled, isAdmin, + canManage, error, apiKeys, } = this.state; - if (permissionDenied) { - return ; - } - if (isLoadingApp) { return ( @@ -103,6 +98,10 @@ export class APIKeysGridPage extends Component { ); } + if (!canManage) { + return ; + } + if (error) { const { body: { error: errorTitle, message, statusCode }, @@ -495,26 +494,25 @@ export class APIKeysGridPage extends Component { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); - this.setState({ isAdmin, areApiKeysEnabled }); + const { + isAdmin, + canManage, + areApiKeysEnabled, + } = await this.props.apiKeysAPIClient.checkPrivileges(); + this.setState({ isAdmin, canManage, areApiKeysEnabled }); - if (areApiKeysEnabled) { - this.initiallyLoadApiKeys(); - } else { - // We're done loading and will just show the "Disabled" error. + if (!canManage || !areApiKeysEnabled) { this.setState({ isLoadingApp: false }); - } - } catch (e) { - if (_.get(e, 'body.statusCode') === 403) { - this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - }) - ); + this.initiallyLoadApiKeys(); } + } catch (e) { + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: e.body?.message ?? '' }, + }) + ); } } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap index a52438ca93638..dd27fe13e84a3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap @@ -29,6 +29,7 @@ exports[`it renders without crashing 1`] = ` } selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index b306de5b84093..46fb9b8572679 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -119,6 +119,7 @@ exports[`it renders without crashing 1`] = ` placeholder="Add a user…" selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap index bbf90d0f64bd2..d23a6da13a3bb 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap @@ -37,6 +37,7 @@ exports[`it renders without crashing 1`] = ` options={Array []} selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> @@ -82,6 +83,7 @@ exports[`it renders without crashing 1`] = ` } selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 122b26378d22b..7c57c4dd997a2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -37,7 +37,7 @@ describe('Security Plugin', () => { ) ).toEqual({ __legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' }, - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), @@ -63,7 +63,7 @@ describe('Security Plugin', () => { expect(setupManagementServiceMock).toHaveBeenCalledTimes(1); expect(setupManagementServiceMock).toHaveBeenCalledWith({ - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), diff --git a/x-pack/plugins/security/server/authentication/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys.test.ts index 836740d0a547f..9f2a628b575d5 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.test.ts @@ -40,6 +40,82 @@ describe('API Keys', () => { }); }); + describe('areAPIKeysEnabled()', () => { + it('returns false when security feature is disabled', async () => { + mockLicense.isEnabled.mockReturnValue(false); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(false); + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('returns false when the exception metadata indicates api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(false); + }); + + it('returns true when the operation completes without error', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValue({}); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(true); + }); + + it('throws the original error when exception metadata does not indicate that api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'something_else' }, + }; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception metadata does not contain `disabled.feature`', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = {}; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception contains no metadata', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('calls callCluster with proper parameters', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValueOnce({}); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(true); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', { + body: { + id: 'kibana-api-key-service-test', + }, + }); + }); + }); + describe('create()', () => { it('returns null when security feature is disabled', async () => { mockLicense.isEnabled.mockReturnValue(false); diff --git a/x-pack/plugins/security/server/authentication/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys.ts index 9df7219cec334..29ff7e1f69f95 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.ts @@ -125,6 +125,35 @@ export class APIKeys { this.license = license; } + /** + * Determines if API Keys are enabled in Elasticsearch. + */ + async areAPIKeysEnabled(): Promise { + if (!this.license.isEnabled()) { + return false; + } + + const id = `kibana-api-key-service-test`; + + this.logger.debug( + `Testing if API Keys are enabled by attempting to invalidate a non-existant key: ${id}` + ); + + try { + await this.clusterClient.callAsInternalUser('shield.invalidateAPIKey', { + body: { + id, + }, + }); + return true; + } catch (e) { + if (this.doesErrorIndicateAPIKeysAreDisabled(e)) { + return false; + } + throw e; + } + } + /** * Tries to create an API key for the current user. * @param request Request instance. @@ -247,6 +276,11 @@ export class APIKeys { return result; } + private doesErrorIndicateAPIKeysAreDisabled(e: Record) { + const disabledFeature = e.body?.error?.['disabled.feature']; + return disabledFeature === 'api_keys'; + } + private getGrantParams(authorizationHeader: HTTPAuthorizationHeader): GrantAPIKeyParams { if (authorizationHeader.scheme.toLowerCase() === 'bearer') { return { diff --git a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts index 1c9b936692f9e..9f1d6b27aa9d7 100644 --- a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts +++ b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks'; +import { httpServerMock } from 'src/core/server/mocks'; import { canRedirectRequest } from './can_redirect_request'; diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 8092c1c81017b..9397a7a42b326 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -11,6 +11,7 @@ export const authenticationMock = { login: jest.fn(), logout: jest.fn(), isProviderTypeEnabled: jest.fn(), + areAPIKeysEnabled: jest.fn(), createAPIKey: jest.fn(), getCurrentUser: jest.fn(), grantAPIKeyAsInternalUser: jest.fn(), diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 5d7b49de68d28..d76a5a533d498 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -172,6 +172,7 @@ export async function setupAuthentication({ getSessionInfo: authenticator.getSessionInfo.bind(authenticator), isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator), getCurrentUser, + areAPIKeysEnabled: () => apiKeys.areAPIKeysEnabled(), createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), grantAPIKeyAsInternalUser: (request: KibanaRequest) => apiKeys.grantAsInternalUser(request), diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index a7a43a3031571..ec50ac090f1e7 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -315,117 +315,123 @@ describe('SAMLAuthenticationProvider', () => { }); }); - it('redirects to the home page if new SAML Response is for the same user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; - - const user = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', - }); - - mockOptions.tokens.invalidate.mockResolvedValue(undefined); - - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/base-path/', { - state: { - username: 'user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); - - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + for (const [description, response] of [ + ['session is valid', Promise.resolve({ username: 'user' })], + [ + 'session is is expired', + Promise.reject(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())), + ], + ] as Array<[string, Promise]>) { + it(`redirects to the home page if new SAML Response is for the same user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, - }); - }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + username: 'user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - const existingUser = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'new-user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - mockOptions.tokens.invalidate.mockResolvedValue(undefined); + it(`redirects to \`overwritten_session\` if new SAML Response is for the another user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { - state: { - username: 'new-user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'new-user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { + state: { + username: 'new-user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - }); + } }); describe('User initiated login with captured redirect URL', () => { diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index e14d34d1901eb..5c5ec49890901 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -158,10 +158,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { return await this.loginWithSAMLResponse(request, samlResponse, state); } - if (authenticationResult.succeeded()) { - // If user has been authenticated via session, but request also includes SAML payload - // we should check whether this payload is for the exactly same user and if not - // we'll re-authenticate user and forward to a page with the respective warning. + // If user has been authenticated via session or failed to do so because of expired access token, + // but request also includes SAML payload we should check whether this payload is for the exactly + // same user and if not we'll re-authenticate user and forward to a page with the respective warning. + if ( + authenticationResult.succeeded() || + (authenticationResult.failed() && + Tokens.isAccessTokenExpiredError(authenticationResult.error)) + ) { return await this.loginWithNewSAMLResponse( request, samlResponse, diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts index 82f29310c04c0..57366183050d7 100644 --- a/x-pack/plugins/security/server/authentication/tokens.test.ts +++ b/x-pack/plugins/security/server/authentication/tokens.test.ts @@ -25,7 +25,7 @@ describe('Tokens', () => { tokens = new Tokens(tokensOptions); }); - it('isAccessTokenExpiredError() returns `true` only if token expired or its document is missing', () => { + it('isAccessTokenExpiredError() returns `true` only if token expired', () => { const nonExpirationErrors = [ {}, new Error(), @@ -91,55 +91,66 @@ describe('Tokens', () => { }); describe('invalidate()', () => { - it('throws if call to delete access token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + for (const [description, failureReason] of [ + ['an unknown error', new Error('failed to delete token')], + ['a 404 error without body', { statusCode: 404 }], + ] as Array<[string, object]>) { + it(`throws if call to delete access token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); - - it('throws if call to delete refresh token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.refresh_token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + it(`throws if call to delete refresh token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.refresh_token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + } it('invalidates all provided tokens', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; @@ -187,23 +198,35 @@ describe('Tokens', () => { ); }); - it('does not fail if none of the tokens were invalidated', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 0 }); - - await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + for (const [description, response] of [ + ['none of the tokens were invalidated', Promise.resolve({ invalidated_tokens: 0 })], + [ + '404 error is returned', + Promise.reject({ statusCode: 404, body: { invalidated_tokens: 0 } }), + ], + ] as Array<[string, Promise]>) { + it(`does not fail if ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation(() => response); + + await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); + }); + } it('does not fail if more than one token per access or refresh token were invalidated', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index ea7b5d5a9ff38..9117c9a679a4a 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -103,8 +103,15 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate refresh token: ${err.message}`); - // We don't re-throw the error here to have a chance to invalidate access token if it's provided. - invalidationError = err; + + // When using already deleted refresh token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + // We don't re-throw the error here to have a chance to invalidate access token if it's provided. + invalidationError = err; + } } if (invalidatedTokensCount === 0) { @@ -128,7 +135,14 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate access token: ${err.message}`); - invalidationError = err; + + // When using already deleted access token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + invalidationError = err; + } } if (invalidatedTokensCount === 0) { diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 4767f57de764c..3ce0198273af9 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -69,6 +69,7 @@ describe('Security Plugin', () => { "registerPrivilegesWithCluster": [Function], }, "authc": Object { + "areAPIKeysEnabled": [Function], "createAPIKey": [Function], "getCurrentUser": [Function], "getSessionInfo": [Function], diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts new file mode 100644 index 0000000000000..3c6dc3c0d7bda --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LicenseCheck } from '../../../../licensing/server'; + +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import Boom from 'boom'; +import { defineEnabledApiKeysRoutes } from './enabled'; +import { APIKeys } from '../../authentication/api_keys'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('API keys enabled', () => { + const enabledApiKeysTest = ( + description: string, + { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + + if (apiResponse) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse); + } + + defineEnabledApiKeysRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key/_enabled', + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.invalidateAPIKey', + { + body: { + id: expect.any(String), + }, + } + ); + } else { + expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + enabledApiKeysTest('returns result of license checker', { + licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + enabledApiKeysTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, + asserts: { statusCode: 406, result: error }, + }); + }); + + describe('success', () => { + enabledApiKeysTest('returns true if API Keys are enabled', { + apiResponse: async () => ({}), + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: true, + }, + }, + }); + enabledApiKeysTest('returns false if API Keys are disabled', { + apiResponse: async () => { + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + throw error; + }, + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: false, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.ts new file mode 100644 index 0000000000000..2f5b8343bcd89 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.ts @@ -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 { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineEnabledApiKeysRoutes({ router, authc }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key/_enabled', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const apiKeysEnabled = await authc.areAPIKeysEnabled(); + + return response.ok({ body: { apiKeysEnabled } }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts index d75eb1bcbe961..7ac37bbead613 100644 --- a/x-pack/plugins/security/server/routes/api_keys/index.ts +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -7,9 +7,11 @@ import { defineGetApiKeysRoutes } from './get'; import { defineCheckPrivilegesRoutes } from './privileges'; import { defineInvalidateApiKeysRoutes } from './invalidate'; +import { defineEnabledApiKeysRoutes } from './enabled'; import { RouteDefinitionParams } from '..'; export function defineApiKeysRoutes(params: RouteDefinitionParams) { + defineEnabledApiKeysRoutes(params); defineGetApiKeysRoutes(params); defineCheckPrivilegesRoutes(params); defineInvalidateApiKeysRoutes(params); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index 311d50e9eb169..afb67dc3bbfca 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -11,25 +11,53 @@ import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import { defineCheckPrivilegesRoutes } from './privileges'; +import { APIKeys } from '../../authentication/api_keys'; interface TestOptions { licenseCheckResult?: LicenseCheck; - apiResponses?: Array<() => Promise>; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + callAsInternalUserResponses?: Array<() => Promise>; + callAsCurrentUserResponses?: Array<() => Promise>; + asserts: { + statusCode: number; + result?: Record; + callAsInternalUserAPIArguments?: unknown[][]; + callAsCurrentUserAPIArguments?: unknown[][]; + }; } describe('Check API keys privileges', () => { const getPrivilegesTest = ( description: string, - { licenseCheckResult = { state: 'valid' }, apiResponses = [], asserts }: TestOptions + { + licenseCheckResult = { state: 'valid' }, + callAsInternalUserResponses = [], + callAsCurrentUserResponses = [], + asserts, + }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of apiResponses) { + for (const apiResponse of callAsCurrentUserResponses) { mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); } + for (const apiResponse of callAsInternalUserResponses) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce( + apiResponse + ); + } defineCheckPrivilegesRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; @@ -48,8 +76,8 @@ describe('Check API keys privileges', () => { expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { + if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) { + for (const apiArguments of asserts.callAsCurrentUserAPIArguments) { expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( mockRequest ); @@ -58,6 +86,17 @@ describe('Check API keys privileges', () => { } else { expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } + + if (Array.isArray(asserts.callAsInternalUserAPIArguments)) { + for (const apiArguments of asserts.callAsInternalUserAPIArguments) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + ...apiArguments + ); + } + } else { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); }; @@ -70,16 +109,21 @@ describe('Check API keys privileges', () => { const error = Boom.notAcceptable('test not acceptable message'); getPrivilegesTest('returns error from cluster client', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => { throw error; }, - async () => {}, ], + callAsInternalUserResponses: [async () => {}], asserts: { - apiArguments: [ - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], - ['shield.getAPIKeys', { owner: true }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 406, result: error, @@ -89,14 +133,16 @@ describe('Check API keys privileges', () => { describe('success', () => { getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => ({ api_keys: [ { @@ -112,71 +158,108 @@ describe('Check API keys privileges', () => { }), ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: true }, + result: { areApiKeysEnabled: true, isAdmin: true, canManage: true }, }, }); getPrivilegesTest( - 'returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', + 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => { - throw Boom.unauthorized('api keys are not enabled'); + const error = new Error(); + (error as any).body = { + error: { + 'disabled.feature': 'api_keys', + }, + }; + throw error; }, ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: false, isAdmin: true }, + result: { areApiKeysEnabled: false, isAdmin: true, canManage: true }, }, } ); getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false }, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, index: {}, application: {}, }), - async () => ({ - api_keys: [ - { - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved', - }, + ], + callAsInternalUserResponses: [async () => ({})], + asserts: { + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: false }, + }, + }); + + getPrivilegesTest('returns canManage=true when user can manage their own API Keys', { + callAsCurrentUserResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, + index: {}, + application: {}, }), ], + callAsInternalUserResponses: [async () => ({})], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: false }, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: true }, }, }); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts index 216d1ef1bf4a4..9cccb96752772 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -8,7 +8,11 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineCheckPrivilegesRoutes({ + router, + clusterClient, + authc, +}: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key/privileges', @@ -20,26 +24,25 @@ export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefi const [ { - cluster: { manage_security: manageSecurity, manage_api_key: manageApiKey }, + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, }, - { areApiKeysEnabled }, + areApiKeysEnabled, ] = await Promise.all([ scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { - body: { cluster: ['manage_security', 'manage_api_key'] }, + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, }), - scopedClusterClient.callAsCurrentUser('shield.getAPIKeys', { owner: true }).then( - // If the API returns a truthy result that means it's enabled. - result => ({ areApiKeysEnabled: !!result }), - // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. - e => - e.message.includes('api keys are not enabled') - ? Promise.resolve({ areApiKeysEnabled: false }) - : Promise.reject(e) - ), + authc.areAPIKeysEnabled(), ]); + const isAdmin = manageSecurity || manageApiKey; + const canManage = manageSecurity || manageApiKey || manageOwnApiKey; + return response.ok({ - body: { areApiKeysEnabled, isAdmin: manageSecurity || manageApiKey }, + body: { areApiKeysEnabled, isAdmin, canManage }, }); } catch (error) { return response.customError(wrapIntoCustomErrorResponse(error)); diff --git a/x-pack/plugins/siem/common/default_index_pattern.ts b/x-pack/plugins/siem/common/default_index_pattern.ts deleted file mode 100644 index 4d53aeb000c55..0000000000000 --- a/x-pack/plugins/siem/common/default_index_pattern.ts +++ /dev/null @@ -1,15 +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. - */ - -/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ -export const defaultIndexPattern = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', -]; diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts index c7bbdbfccf082..b46ccdbbe3d05 100644 --- a/x-pack/plugins/siem/common/utility_types.ts +++ b/x-pack/plugins/siem/common/utility_types.ts @@ -6,12 +6,6 @@ import { ReactNode } from 'react'; -export type Pick3 = { - [P1 in K1]: { [P2 in K2]: { [P3 in K3]: T[K1][K2][P3] } }; -}; - -export type Omit = Pick>; - // This type is for typing EuiDescriptionList export interface DescriptionList { title: NonNullable; diff --git a/x-pack/plugins/siem/cypress/integration/cases.spec.ts b/x-pack/plugins/siem/cypress/integration/cases.spec.ts new file mode 100644 index 0000000000000..f541555d56440 --- /dev/null +++ b/x-pack/plugins/siem/cypress/integration/cases.spec.ts @@ -0,0 +1,115 @@ +/* + * 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 { case1 } from '../objects/case'; + +import { + ALL_CASES_CLOSE_ACTION, + ALL_CASES_CLOSED_CASES_COUNT, + ALL_CASES_CLOSED_CASES_STATS, + ALL_CASES_COMMENTS_COUNT, + ALL_CASES_DELETE_ACTION, + ALL_CASES_NAME, + ALL_CASES_OPEN_CASES_COUNT, + ALL_CASES_OPEN_CASES_STATS, + ALL_CASES_OPENED_ON, + ALL_CASES_PAGE_TITLE, + ALL_CASES_REPORTER, + ALL_CASES_REPORTERS_COUNT, + ALL_CASES_SERVICE_NOW_INCIDENT, + ALL_CASES_TAGS, + ALL_CASES_TAGS_COUNT, +} from '../screens/all_cases'; +import { + ACTION, + CASE_DETAILS_DESCRIPTION, + CASE_DETAILS_PAGE_TITLE, + CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN, + CASE_DETAILS_STATUS, + CASE_DETAILS_TAGS, + CASE_DETAILS_TIMELINE_MARKDOWN, + CASE_DETAILS_USER_ACTION, + CASE_DETAILS_USERNAMES, + PARTICIPANTS, + REPORTER, + USER, +} from '../screens/case_details'; +import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens/timeline'; + +import { goToCaseDetails, goToCreateNewCase } from '../tasks/all_cases'; +import { openCaseTimeline } from '../tasks/case_details'; +import { backToCases, createNewCase } from '../tasks/create_new_case'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; + +import { CASES } from '../urls/navigation'; + +describe('Cases', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Creates a new case with timeline and opens the timeline', () => { + loginAndWaitForPageWithoutDateRange(CASES); + goToCreateNewCase(); + createNewCase(case1); + backToCases(); + + cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta'); + cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1'); + cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', 'Closed cases0'); + cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open cases (1)'); + cy.get(ALL_CASES_CLOSED_CASES_COUNT).should('have.text', 'Closed cases (0)'); + cy.get(ALL_CASES_REPORTERS_COUNT).should('have.text', 'Reporter1'); + cy.get(ALL_CASES_TAGS_COUNT).should('have.text', 'Tags2'); + cy.get(ALL_CASES_NAME).should('have.text', case1.name); + cy.get(ALL_CASES_REPORTER).should('have.text', case1.reporter); + case1.tags.forEach((tag, index) => { + cy.get(ALL_CASES_TAGS(index)).should('have.text', tag); + }); + cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); + cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago'); + cy.get(ALL_CASES_SERVICE_NOW_INCIDENT).should('have.text', 'Not pushed'); + cy.get(ALL_CASES_DELETE_ACTION).should('exist'); + cy.get(ALL_CASES_CLOSE_ACTION).should('exist'); + + goToCaseDetails(); + + const expectedTags = case1.tags.join(''); + cy.get(CASE_DETAILS_PAGE_TITLE).should('have.text', case1.name); + cy.get(CASE_DETAILS_STATUS).should('have.text', 'open'); + cy.get(CASE_DETAILS_USER_ACTION) + .eq(USER) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_USER_ACTION) + .eq(ACTION) + .should('have.text', 'added description'); + cy.get(CASE_DETAILS_DESCRIPTION).should( + 'have.text', + `${case1.description} ${case1.timeline.title}` + ); + cy.get(CASE_DETAILS_USERNAMES) + .eq(REPORTER) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_USERNAMES) + .eq(PARTICIPANTS) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_TAGS).should('have.text', expectedTags); + cy.get(CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN).should('have.attr', 'disabled'); + cy.get(CASE_DETAILS_TIMELINE_MARKDOWN).then($element => { + const timelineLink = $element.prop('href').match(/http(s?):\/\/\w*:\w*(\S*)/)[0]; + openCaseTimeline(timelineLink); + + cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title); + cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description); + cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query); + }); + }); +}); diff --git a/x-pack/plugins/siem/cypress/objects/case.ts b/x-pack/plugins/siem/cypress/objects/case.ts new file mode 100644 index 0000000000000..1c7bc34bca417 --- /dev/null +++ b/x-pack/plugins/siem/cypress/objects/case.ts @@ -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. + */ + +import { Timeline } from './timeline'; + +export interface TestCase { + name: string; + tags: string[]; + description: string; + timeline: Timeline; + reporter: string; +} + +const caseTimeline: Timeline = { + title: 'SIEM test', + description: 'description', + query: 'host.name:*', +}; + +export const case1: TestCase = { + name: 'This is the title of the case', + tags: ['Tag1', 'Tag2'], + description: 'This is the case description', + timeline: caseTimeline, + reporter: 'elastic', +}; diff --git a/x-pack/plugins/siem/cypress/objects/timeline.ts b/x-pack/plugins/siem/cypress/objects/timeline.ts index bca99bfa9266a..060a1376b46ce 100644 --- a/x-pack/plugins/siem/cypress/objects/timeline.ts +++ b/x-pack/plugins/siem/cypress/objects/timeline.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Timeline { +export interface Timeline { title: string; + description: string; query: string; } diff --git a/x-pack/plugins/siem/cypress/screens/all_cases.ts b/x-pack/plugins/siem/cypress/screens/all_cases.ts new file mode 100644 index 0000000000000..b1e4c66515352 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/all_cases.ts @@ -0,0 +1,41 @@ +/* + * 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 const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]'; + +export const ALL_CASES_CLOSED_CASES_COUNT = '[data-test-subj="closed-case-count"]'; + +export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"]'; + +export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]'; + +export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn"]'; + +export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]'; + +export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]'; + +export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="open-case-count"]'; + +export const ALL_CASES_OPEN_CASES_STATS = '[data-test-subj="openStatsHeader"]'; + +export const ALL_CASES_OPENED_ON = '[data-test-subj="case-table-column-createdAt"]'; + +export const ALL_CASES_PAGE_TITLE = '[data-test-subj="header-page-title"]'; + +export const ALL_CASES_REPORTER = '[data-test-subj="case-table-column-createdBy"]'; + +export const ALL_CASES_REPORTERS_COUNT = + '[data-test-subj="options-filter-popover-button-Reporter"]'; + +export const ALL_CASES_SERVICE_NOW_INCIDENT = + '[data-test-subj="case-table-column-external-notPushed"]'; + +export const ALL_CASES_TAGS = (index: number) => { + return `[data-test-subj="case-table-column-tags-${index}"]`; +}; + +export const ALL_CASES_TAGS_COUNT = '[data-test-subj="options-filter-popover-button-Tags"]'; diff --git a/x-pack/plugins/siem/cypress/screens/case_details.ts b/x-pack/plugins/siem/cypress/screens/case_details.ts new file mode 100644 index 0000000000000..3bd180b1d588f --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/case_details.ts @@ -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. + */ + +export const ACTION = 2; + +export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="markdown-root"]'; + +export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]'; + +export const CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN = '[data-test-subj="push-to-service-now"]'; + +export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status"]'; + +export const CASE_DETAILS_TAGS = '[data-test-subj="case-tags"]'; + +export const CASE_DETAILS_TIMELINE_MARKDOWN = '[data-test-subj="markdown-link"]'; + +export const CASE_DETAILS_USER_ACTION = '[data-test-subj="user-action-title"] .euiFlexItem'; + +export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]'; + +export const PARTICIPANTS = 1; + +export const REPORTER = 0; + +export const USER = 1; diff --git a/x-pack/plugins/siem/cypress/screens/create_new_case.ts b/x-pack/plugins/siem/cypress/screens/create_new_case.ts new file mode 100644 index 0000000000000..6e2beb78fff19 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/create_new_case.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]'; + +export const DESCRIPTION_INPUT = + '[data-test-subj="caseDescription"] [data-test-subj="textAreaInput"]'; + +export const INSERT_TIMELINE_BTN = '[data-test-subj="insert-timeline-button"]'; + +export const LOADING_SPINNER = '[data-test-subj="create-case-loading-spinner"]'; + +export const SUBMIT_BTN = '[data-test-subj="create-case-submit"]'; + +export const TAGS_INPUT = '[data-test-subj="caseTags"] [data-test-subj="comboBoxSearchInput"]'; + +export const TIMELINE = '[data-test-subj="timeline"]'; + +export const TIMELINE_SEARCHBOX = '[data-test-subj="timeline-super-select-search-box"]'; + +export const TITLE_INPUT = '[data-test-subj="caseTitle"] [data-test-subj="input"]'; diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts index 53d8273d9ce6b..58d2568084f7c 100644 --- a/x-pack/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/plugins/siem/cypress/screens/timeline.ts @@ -42,6 +42,8 @@ export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]' export const TIMELINE_NOT_READY_TO_DROP_BUTTON = '[data-test-subj="flyout-button-not-ready-to-drop"]'; +export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; + export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; diff --git a/x-pack/plugins/siem/cypress/tasks/all_cases.ts b/x-pack/plugins/siem/cypress/tasks/all_cases.ts new file mode 100644 index 0000000000000..f374532201324 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/all_cases.ts @@ -0,0 +1,15 @@ +/* + * 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 { ALL_CASES_NAME, ALL_CASES_CREATE_NEW_CASE_BTN } from '../screens/all_cases'; + +export const goToCreateNewCase = () => { + cy.get(ALL_CASES_CREATE_NEW_CASE_BTN).click({ force: true }); +}; + +export const goToCaseDetails = () => { + cy.get(ALL_CASES_NAME).click({ force: true }); +}; diff --git a/x-pack/plugins/siem/cypress/tasks/case_details.ts b/x-pack/plugins/siem/cypress/tasks/case_details.ts new file mode 100644 index 0000000000000..a28f8b8010adb --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/case_details.ts @@ -0,0 +1,14 @@ +/* + * 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 { TIMELINE_TITLE } from '../screens/timeline'; + +export const openCaseTimeline = (link: string) => { + cy.visit('/app/kibana'); + cy.visit(link); + cy.contains('a', 'SIEM'); + cy.get(TIMELINE_TITLE).should('exist'); +}; diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_case.ts b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts new file mode 100644 index 0000000000000..b7078a1033de8 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TestCase } from '../objects/case'; + +import { + BACK_TO_CASES_BTN, + DESCRIPTION_INPUT, + SUBMIT_BTN, + INSERT_TIMELINE_BTN, + LOADING_SPINNER, + TAGS_INPUT, + TIMELINE, + TIMELINE_SEARCHBOX, + TITLE_INPUT, +} from '../screens/create_new_case'; + +export const backToCases = () => { + cy.get(BACK_TO_CASES_BTN).click({ force: true }); +}; + +export const createNewCase = (newCase: TestCase) => { + cy.get(TITLE_INPUT).type(newCase.name, { force: true }); + newCase.tags.forEach(tag => { + cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); + }); + cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); + + cy.get(INSERT_TIMELINE_BTN).click({ force: true }); + cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`); + cy.get(TIMELINE).should('be.visible'); + cy.get(TIMELINE) + .eq(1) + .click({ force: true }); + + cy.get(SUBMIT_BTN).click({ force: true }); + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); +}; diff --git a/x-pack/plugins/siem/cypress/urls/navigation.ts b/x-pack/plugins/siem/cypress/urls/navigation.ts index 5e65e5aa34c18..263469a4dbaed 100644 --- a/x-pack/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/plugins/siem/cypress/urls/navigation.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CASES = '/app/siem#/case'; export const DETECTIONS = 'app/siem#/detections'; export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { diff --git a/x-pack/plugins/siem/package.json b/x-pack/plugins/siem/package.json index 1fcef46243628..31c930dce71c0 100644 --- a/x-pack/plugins/siem/package.json +++ b/x-pack/plugins/siem/package.json @@ -5,7 +5,7 @@ "private": true, "license": "Elastic-License", "scripts": { - "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js & node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;", diff --git a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js index 478463b1a8064..145d9715970c8 100644 --- a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js @@ -123,7 +123,7 @@ async function main() { .replace(/}"/g, '}') .replace(/"{/g, '{')}; - export const techniques = ${JSON.stringify(techniques, null, 2)}; + export const technique = ${JSON.stringify(techniques, null, 2)}; export const techniquesOptions: MitreTechniquesOptions[] = ${JSON.stringify(getTechniquesOptions(techniques), null, 2) diff --git a/x-pack/plugins/siem/server/client/client.test.ts b/x-pack/plugins/siem/server/client/client.test.ts index 94ff2149b8c64..c0ae15cb73f4e 100644 --- a/x-pack/plugins/siem/server/client/client.test.ts +++ b/x-pack/plugins/siem/server/client/client.test.ts @@ -9,7 +9,7 @@ import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { SiemClient } from './client'; describe('SiemClient', () => { - describe('#signalsIndex', () => { + describe('#getSignalsIndex', () => { it('returns the index scoped to the specified spaceId', () => { const mockConfig = { ...createMockConfig(), @@ -18,7 +18,7 @@ describe('SiemClient', () => { const spaceId = 'fooSpace'; const client = new SiemClient(spaceId, mockConfig); - expect(client.signalsIndex).toEqual('mockSignalsIndex-fooSpace'); + expect(client.getSignalsIndex()).toEqual('mockSignalsIndex-fooSpace'); }); }); }); diff --git a/x-pack/plugins/siem/server/client/client.ts b/x-pack/plugins/siem/server/client/client.ts index 6cb0d4cfade77..5780bb4173f79 100644 --- a/x-pack/plugins/siem/server/client/client.ts +++ b/x-pack/plugins/siem/server/client/client.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConfigType } from '..'; +import { ConfigType } from '../config'; export class SiemClient { - public readonly signalsIndex: string; + private readonly signalsIndex: string; constructor(private spaceId: string, private config: ConfigType) { const configuredSignalsIndex = this.config.signalsIndex; this.signalsIndex = `${configuredSignalsIndex}-${this.spaceId}`; } + + public getSignalsIndex = (): string => this.signalsIndex; } diff --git a/x-pack/plugins/siem/server/client/factory.ts b/x-pack/plugins/siem/server/client/factory.ts index d3d6b84e5b090..69db4d7eed98f 100644 --- a/x-pack/plugins/siem/server/client/factory.ts +++ b/x-pack/plugins/siem/server/client/factory.ts @@ -6,7 +6,7 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SiemClient } from './client'; -import { ConfigType } from '..'; +import { ConfigType } from '../config'; interface SetupDependencies { getSpaceId?: (request: KibanaRequest) => string | undefined; diff --git a/x-pack/plugins/siem/server/index.ts b/x-pack/plugins/siem/server/index.ts index 83e2f900a3b90..e9cd78589fac9 100644 --- a/x-pack/plugins/siem/server/index.ts +++ b/x-pack/plugins/siem/server/index.ts @@ -5,7 +5,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { Plugin, PluginSetup, PluginStart } from './plugin'; import { configSchema, ConfigType } from './config'; export const plugin = (context: PluginInitializerContext) => { @@ -14,4 +14,4 @@ export const plugin = (context: PluginInitializerContext) => { export const config = { schema: configSchema }; -export { ConfigType }; +export { ConfigType, Plugin, PluginSetup, PluginStart }; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index 6244a4cc64e68..e8d778bddadc2 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult } from '../routes/__mocks__/request_responses'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; import { buildSignalsSearchQuery } from './build_signals_query'; @@ -15,12 +15,12 @@ jest.mock('./build_signals_query'); describe('rules_notification_alert_type', () => { let payload: NotificationExecutorOptions; let alert: ReturnType; - let logger: ReturnType; + let logger: ReturnType; let alertServices: AlertServicesMock; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); payload = { alertId: '1111', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts index 4fce037b483d5..0c9ccf069b3b6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getNotificationResult, getResult } from '../routes/__mocks__/request_responses'; import { isAlertTypes, isNotificationAlertExecutor } from './types'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; @@ -20,7 +20,9 @@ describe('types', () => { it('isNotificationAlertExecutor should return true it passed object is NotificationAlertTypeDefinition type', () => { expect( - isNotificationAlertExecutor(rulesNotificationAlertType({ logger: loggerMock.create() })) + isNotificationAlertExecutor( + rulesNotificationAlertType({ logger: loggingServiceMock.createLogger() }) + ) ).toEqual(true); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts index 10efdb518f7b7..f3b4068f6dd2d 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -13,6 +13,7 @@ import { import { alertsClientMock } from '../../../../../../alerting/server/mocks'; import { actionsClientMock } from '../../../../../../actions/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; +import { siemMock } from '../../../../mocks'; const createMockClients = () => ({ actionsClient: actionsClientMock.create(), @@ -20,7 +21,7 @@ const createMockClients = () => ({ clusterClient: elasticsearchServiceMock.createScopedClusterClient(), licensing: { license: licensingMock.createLicenseMock() }, savedObjectsClient: savedObjectsClientMock.create(), - siemClient: { signalsIndex: 'mockSignalsIndex' }, + siemClient: siemMock.createClient(), }); const createRequestContextMock = ( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index cb48e35228858..20b8ad29d2715 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -37,7 +37,7 @@ export const createIndexRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(callCluster, index); if (indexExists) { return siemResponse.error({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index 5eff38b778492..79cf4851f9ab8 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -45,7 +45,7 @@ export const deleteIndexRoute = (router: IRouter) => { } const callCluster = clusterClient.callAsCurrentUser; - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(callCluster, index); if (!indexExists) { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 8ff8d7461ecd1..2b418892f0f39 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -29,7 +29,7 @@ export const readIndexRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); if (indexExists) { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 7dbbe837e656d..e3c41c555f297 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -36,7 +36,7 @@ export const readPrivilegesRoute = ( return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); const privileges = merge(clusterPrivileges, { is_authenticated: security?.authc.isAuthenticated(request) ?? false, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index bfc8c9c54b2c0..3c6adce45f959 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -49,7 +49,7 @@ export const addPrepackedRulesRoute = (router: IRouter) => { const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const { signalsIndex } = siemClient; + const signalsIndex = siemClient.getSignalsIndex(); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { const signalsIndexExists = await getIndexExists( clusterClient.callAsCurrentUser, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 2d7ddb79e5af5..133c98a6af7b3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -92,7 +92,7 @@ export const createRulesBulkRoute = (router: IRouter) => { try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 1f0896686aca0..9f1cddb2051c9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -9,10 +9,8 @@ import uuid from 'uuid'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; import { RuleAlertParamsRest } from '../../types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { transformValidate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { createRulesSchema } from '../schemas/create_rules_schema'; @@ -23,6 +21,7 @@ import { validateLicenseForRuleType, } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const createRulesRoute = (router: IRouter): void => { router.post( @@ -82,7 +81,7 @@ export const createRulesRoute = (router: IRouter): void => { return siemResponse.error({ statusCode: 404 }); } - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return siemResponse.error({ @@ -145,10 +144,7 @@ export const createRulesRoute = (router: IRouter): void => { name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 38748e287ab45..b35ba27ef3561 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -11,14 +11,11 @@ import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; -import { - IRuleSavedAttributesSavedObjectAttributes, - DeleteRulesRequestParams, -} from '../../rules/types'; +import { DeleteRulesRequestParams } from '../../rules/types'; import { deleteRules } from '../../rules/delete_rules'; import { deleteNotifications } from '../../notifications/delete_notifications'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; type Config = RouteConfig; type Handler = RequestHandler; @@ -44,6 +41,8 @@ export const deleteRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const rules = await Promise.all( request.body.map(async payloadRule => { const { id, rule_id: ruleId } = payloadRule; @@ -61,17 +60,12 @@ export const deleteRulesBulkRoute = (router: IRouter) => { ruleAlertId: rule.id, savedObjectsClient, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 6, search: rule.id, searchFields: ['alertId'], }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); + ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id)); return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 098d556741fed..2288633ee8d2e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -11,13 +11,10 @@ import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; -import { - DeleteRuleRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { DeleteRuleRequestParams } from '../../rules/types'; import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const deleteRulesRoute = (router: IRouter) => { router.delete( @@ -44,6 +41,7 @@ export const deleteRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await deleteRules({ actionsClient, alertsClient, @@ -56,17 +54,12 @@ export const deleteRulesRoute = (router: IRouter) => { ruleAlertId: rule.id, savedObjectsClient, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 6, search: rule.id, searchFields: ['alertId'], }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); + ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id)); const [validated, errors] = transformValidate( rule, undefined, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index 8433b74adf310..bc4568dd0a40b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -6,7 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ConfigType } from '../../../..'; +import { ConfigType } from '../../../../config'; import { ExportRulesRequestParams } from '../../rules/types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 9661fac81497c..f293b9e64a316 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -7,15 +7,12 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; -import { - FindRulesRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { FindRulesRequestParams } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; import { transformValidateFindAlerts } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const findRulesRoute = (router: IRouter) => { router.get( @@ -40,6 +37,7 @@ export const findRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await findRules({ alertsClient, perPage: query.per_page, @@ -50,10 +48,7 @@ export const findRulesRoute = (router: IRouter) => { }); const ruleStatuses = await Promise.all( rules.data.map(async rule => { - const results = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const results = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 6b54a25a1b1c4..8e35fecf6a652 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -9,17 +9,16 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequestParams, - IRuleSavedAttributesSavedObjectAttributes, RuleStatusResponse, IRuleStatusAttributes, } from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { buildRouteValidation, transformError, convertToSnakeCase, buildSiemResponse, } from '../utils'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const findRulesStatusesRoute = (router: IRouter) => { router.post( @@ -50,12 +49,10 @@ export const findRulesStatusesRoute = (router: IRouter) => { } */ try { + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const statuses = await body.ids.reduce>( async (acc, id) => { - const lastFiveErrorsForId = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const lastFiveErrorsForId = await ruleStatusClient.find({ perPage: 6, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 8c052cfdf4024..1233e01a67762 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -126,6 +126,7 @@ describe('import_rules_route', () => { }); test('returns an error if the index does not exist', async () => { + clients.siemClient.getSignalsIndex.mockReturnValue('mockSignalsIndex'); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); const response = await server.inject(request, context); expect(response.status).toEqual(200); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 527fab786910f..202252da293ee 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -10,7 +10,7 @@ import { extname } from 'path'; import { IRouter } from '../../../../../../../../src/core/server'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ConfigType } from '../../../..'; +import { ConfigType } from '../../../../config'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequestParams } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; @@ -147,7 +147,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { ruleType: type, }); - const signalsIndex = siemClient.signalsIndex; + const signalsIndex = siemClient.getSignalsIndex(); const indexExists = await getIndexExists( clusterClient.callAsCurrentUser, signalsIndex diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index e4236f4632dcd..534253db65d78 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - IRuleSavedAttributesSavedObjectAttributes, - PatchRuleAlertParamsRest, -} from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { transformBulkError, buildRouteValidation, @@ -21,8 +18,8 @@ import { transformValidateBulkError, validate } from './validate'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const patchRulesBulkRoute = (router: IRouter) => { router.patch( @@ -46,6 +43,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { const { @@ -131,10 +129,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { throttle, name: rule.name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 23469144e11f8..f7932cb016ba7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -7,10 +7,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRules } from '../../rules/patch_rules'; -import { - PatchRuleAlertParamsRest, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { patchRulesSchema } from '../schemas/patch_rules_schema'; import { buildRouteValidation, @@ -20,8 +17,8 @@ import { } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const patchRulesRoute = (router: IRouter) => { router.patch( @@ -83,6 +80,7 @@ export const patchRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await patchRules({ actionsClient, alertsClient, @@ -127,10 +125,7 @@ export const patchRulesRoute = (router: IRouter) => { throttle, name: rule.name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index 4d23e0217f2e8..cedd7ccd1a411 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -11,12 +11,9 @@ import { transformValidate } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; import { readRules } from '../../rules/read_rules'; import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { - ReadRuleRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { ReadRuleRequestParams } from '../../rules/types'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const readRulesRoute = (router: IRouter) => { router.get( @@ -41,6 +38,7 @@ export const readRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await readRules({ alertsClient, id, @@ -51,10 +49,7 @@ export const readRulesRoute = (router: IRouter) => { savedObjectsClient, ruleAlertId: rule.id, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 6db91d74294fc..f929f2fb3f649 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - IRuleSavedAttributesSavedObjectAttributes, - UpdateRuleAlertParamsRest, -} from '../../rules/types'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { @@ -19,10 +16,10 @@ import { validateLicenseForRuleType, } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const updateRulesBulkRoute = (router: IRouter) => { router.put( @@ -47,6 +44,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { const { @@ -83,7 +81,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { version, exceptions_list, } = payloadRule; - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); @@ -134,10 +132,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { throttle, name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 7dbbe5a22ab46..dedc2c914410a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - UpdateRuleAlertParamsRest, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; import { buildRouteValidation, @@ -19,9 +16,9 @@ import { } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const updateRulesRoute = (router: IRouter) => { router.put( @@ -78,12 +75,13 @@ export const updateRulesRoute = (router: IRouter) => { const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.siem?.getSiemClient(); + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const rule = await updateRules({ alertsClient, actionsClient, @@ -131,10 +129,7 @@ export const updateRulesRoute = (router: IRouter) => { throttle, name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts index c207d075331b6..cfba40fc225a2 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -9,8 +9,9 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { formatErrors } from '../../../../utils/build_validation/format_errors'; +import { exactCheck } from '../../../../utils/build_validation/exact_check'; import { PartialAlert, FindResult } from '../../../../../../alerting/server'; -import { formatErrors } from '../schemas/response/utils'; import { isAlertType, IRuleSavedAttributesSavedObjectAttributes, @@ -19,7 +20,6 @@ import { import { OutputRuleAlertRest } from '../../types'; import { createBulkErrorObject, BulkError } from '../utils'; import { rulesSchema, RulesSchema } from '../schemas/response/rules_schema'; -import { exactCheck } from '../schemas/response/exact_check'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; import { findRulesSchema } from '../schemas/response/find_rules_schema'; import { RuleActions } from '../../rule_actions/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index 21f18f9db55fb..fef6bcf42e49f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -4,32 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; -import { fold } from 'fp-ts/lib/Either'; import { RulesSchema } from '../rules_schema'; import { RulesBulkSchema } from '../rules_bulk_schema'; import { ErrorSchema } from '../error_schema'; import { FindRulesSchema } from '../find_rules_schema'; -import { formatErrors } from '../utils'; -import { pipe } from 'fp-ts/lib/pipeable'; - -interface Message { - errors: t.Errors; - schema: T | {}; -} - -const onLeft = (errors: t.Errors): Message => { - return { schema: {}, errors }; -}; - -const onRight = (schema: T): Message => { - return { - schema, - errors: [], - }; -}; - -export const foldLeftRight = fold(onLeft, onRight); export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; @@ -127,18 +105,3 @@ export const getFindResponseSingle = (): FindRulesSchema => ({ total: 1, data: [getBaseResponsePayload()], }); - -/** - * Convenience utility to keep the error message handling within tests to be - * very concise. - * @param validation The validation to get the errors from - */ -export const getPaths = (validation: t.Validation): string[] => { - return pipe( - validation, - fold( - errors => formatErrors(errors), - () => ['no errors'] - ) - ); -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts index 0eda2a7a13d96..fbd2382e2826d 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts @@ -15,17 +15,13 @@ import { addQueryFields, addMlFields, } from './check_type_dependents'; -import { - foldLeftRight, - getBaseResponsePayload, - getPaths, - getMlRuleResponsePayload, -} from './__mocks__/utils'; +import { getBaseResponsePayload, getMlRuleResponsePayload } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck } from './exact_check'; import { RulesSchema } from './rules_schema'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; describe('check_type_dependents', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts index 11d8b85f25920..6e159a792edb6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts @@ -7,10 +7,11 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; -import { foldLeftRight, getErrorPayload, getPaths } from './__mocks__/utils'; +import { getErrorPayload } from './__mocks__/utils'; import { errorSchema, ErrorSchema } from './error_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('error_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts deleted file mode 100644 index 6fa0472950189..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts +++ /dev/null @@ -1,84 +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 * as t from 'io-ts'; -import { left, Either, fold, right } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { isObject, get } from 'lodash/fp'; - -/** - * Given an original object and a decoded object this will return an error - * if and only if the original object has additional keys that the decoded - * object does not have. If the original decoded already has an error, then - * this will return the error as is and not continue. - * - * NOTE: You MUST use t.exact(...) for this to operate correctly as your schema - * needs to remove additional keys before the compare - * - * You might not need this in the future if the below issue is solved: - * https://github.com/gcanti/io-ts/issues/322 - * - * @param original The original to check if it has additional keys - * @param decoded The decoded either which has either an existing error or the - * decoded object which could have additional keys stripped from it. - */ -export const exactCheck = ( - original: object, - decoded: Either -): Either => { - const onLeft = (errors: t.Errors): Either => left(errors); - const onRight = (decodedValue: T): Either => { - const differences = findDifferencesRecursive(original, decodedValue); - if (differences.length !== 0) { - const validationError: t.ValidationError = { - value: differences, - context: [], - message: `invalid keys "${differences.join(',')}"`, - }; - const error: t.Errors = [validationError]; - return left(error); - } else { - return right(decodedValue); - } - }; - return pipe(decoded, fold(onLeft, onRight)); -}; - -export const findDifferencesRecursive = (original: object, decodedValue: T): string[] => { - if (decodedValue == null) { - try { - // It is null and painful when the original contains an object or an array - // the the decoded value does not have. - return [JSON.stringify(original)]; - } catch (err) { - return ['circular reference']; - } - } - const decodedKeys = Object.keys(decodedValue); - const differences = Object.keys(original).flatMap(originalKey => { - const foundKey = decodedKeys.some(key => key === originalKey); - const topLevelKey = foundKey ? [] : [originalKey]; - // I use lodash to cheat and get an any (not going to lie ;-)) - const valueObjectOrArrayOriginal = get(originalKey, original); - const valueObjectOrArrayDecoded = get(originalKey, decodedValue); - if (isObject(valueObjectOrArrayOriginal)) { - return [ - ...topLevelKey, - ...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), - ]; - } else if (Array.isArray(valueObjectOrArrayOriginal)) { - return [ - ...topLevelKey, - ...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => - findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) - ), - ]; - } else { - return topLevelKey; - } - }); - return differences; -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts index f5c1970ee8c55..68b67db595d76 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts @@ -5,17 +5,13 @@ */ import { findRulesSchema, FindRulesSchema } from './find_rules_schema'; -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - foldLeftRight, - getFindResponseSingle, - getBaseResponsePayload, - getPaths, -} from './__mocks__/utils'; +import { getFindResponseSingle, getBaseResponsePayload } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { RulesSchema } from './rules_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { getPaths, foldLeftRight } from '../../../../../utils/build_validation/__mocks__/utils'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; describe('find_rules_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts index ce4bbf420a634..b0b863ebbbc0b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; -import { left } from 'fp-ts/lib/Either'; +import { left, Either } from 'fp-ts/lib/Either'; import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; import { ErrorSchema } from './error_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { getPaths, foldLeftRight } from '../../../../../utils/build_validation/__mocks__/utils'; +import { Errors } from 'io-ts'; describe('import_rules_schema', () => { beforeAll(() => { @@ -79,13 +80,31 @@ describe('import_rules_schema', () => { }); test('it should NOT validate a success that is not a boolean', () => { + type UnsafeCastForTest = Either< + Errors, + { + success: string; + success_count: number; + errors: Array< + { + id?: string | undefined; + rule_id?: string | undefined; + } & { + error: { + status_code: number; + message: string; + }; + } + >; + } + >; const payload: Omit & { success: string } = { success: 'hello', success_count: 0, errors: [], }; const decoded = importRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); + const checked = exactCheck(payload, decoded as UnsafeCastForTest); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "hello" supplied to "success"']); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts index 46667826416e1..827167c63fd58 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rules_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts index 1c270ff402f75..a864667583c0a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesStatusSchema, prePackagedRulesStatusSchema, } from './prepackaged_rules_status_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rules_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts index 8dc97d727c4d1..9a7cf5e2c2871 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts @@ -7,17 +7,13 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; -import { - foldLeftRight, - getBaseResponsePayload, - getErrorPayload, - getPaths, -} from './__mocks__/utils'; +import { getBaseResponsePayload, getErrorPayload } from './__mocks__/utils'; import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rule_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts index 4bfc51c1a66aa..82a6682e6461f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts @@ -7,10 +7,11 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; import { rulesSchema, RulesSchema, removeList } from './rules_schema'; -import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; +import { getBaseResponsePayload } from './__mocks__/utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts index 68a3c8b303823..85fb124464487 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts @@ -7,10 +7,10 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rule_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts deleted file mode 100644 index c1eb32be4895c..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts +++ /dev/null @@ -1,139 +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 * as t from 'io-ts'; -import { formatErrors } from './utils'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; - -describe('utils', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('returns an empty error message string if there are no errors', () => { - const errors: t.Errors = []; - const output = formatErrors(errors); - expect(output).toEqual([]); - }); - - test('returns a single error message if given one', () => { - const validationError: t.ValidationError = { - value: 'Some existing error', - context: [], - message: 'some error', - }; - const errors: t.Errors = [validationError]; - const output = formatErrors(errors); - expect(output).toEqual(['some error']); - }); - - test('returns a two error messages if given two', () => { - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context: [], - message: 'some error 1', - }; - const validationError2: t.ValidationError = { - value: 'Some existing error 2', - context: [], - message: 'some error 2', - }; - const errors: t.Errors = [validationError1, validationError2]; - const output = formatErrors(errors); - expect(output).toEqual(['some error 1', 'some error 2']); - }); - - test('will use message before context if it is set', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - message: 'I should be used first', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['I should be used first']); - }); - - test('will use context entry of a single string', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); - }); - - test('will use two context entries of two strings', () => { - const context: t.Context = ([ - { key: 'some string key 1' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', - ]); - }); - - test('will filter out and not use any strings of numbers', () => { - const context: t.Context = ([ - { key: '5' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use null', () => { - const context: t.Context = ([ - { key: null }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use empty strings', () => { - const context: t.Context = ([ - { key: '' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); -}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts index fbafaf7f52ecb..ff62ea4443f3f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts @@ -6,8 +6,8 @@ import { IsoDateString } from './iso_date_string'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('ios_date_string', () => { test('it should validate a iso string', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts index e8a9c7b0886a1..2a97c8a4a143e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts @@ -6,8 +6,8 @@ import { ListsDefaultArray } from './lists_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('lists_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts index bc17303f24203..d6f21681df88f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts @@ -6,8 +6,8 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts index cee451279663a..26441745a7f29 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts @@ -6,8 +6,8 @@ import { PositiveInteger } from './positive_integer'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts index 3ae8415b4f170..76f722274ce23 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts @@ -6,8 +6,8 @@ import { ReferencesDefaultArray } from './references_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('references_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts index ab3f80944489f..76e3445358dd9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts @@ -6,8 +6,8 @@ import { RiskScore } from './risk_score'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('risk_score', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts index 342e6f2db2e16..7b68dbcef2d7e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts @@ -6,8 +6,8 @@ import { UUID } from './uuid'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('uuid', () => { test('it should validate a uuid', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index c71761fcc39db..bcb70b6b4f0dd 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -44,7 +44,7 @@ export const setSignalsStatusRoute = (router: IRouter) => { } try { const result = await clusterClient.callAsCurrentUser('updateByQuery', { - index: siemClient.signalsIndex, + index: siemClient.getSignalsIndex(), body: { script: { source: `ctx._source.signal.status = '${status}'`, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index fd02b3371ed38..41896c725b903 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -29,7 +29,7 @@ export const querySignalsRoute = (router: IRouter) => { try { const result = await clusterClient.callAsCurrentUser('search', { - index: siemClient.signalsIndex, + index: siemClient.getSignalsIndex(), body: { query, aggs, _source, track_total_hits, size }, ignoreUnavailable: true, }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts index f54f43c41ef6e..d50c339c95266 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts @@ -4,37 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsType } from '../../../../../../../src/core/server'; + export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; export const ruleActionsSavedObjectMappings = { - [ruleActionsSavedObjectType]: { - properties: { - alertThrottle: { - type: 'keyword', - }, - ruleAlertId: { - type: 'keyword', - }, - ruleThrottle: { - type: 'keyword', - }, - actions: { - properties: { - group: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - action_type_id: { - type: 'keyword', - }, - params: { - dynamic: true, - properties: {}, - }, + properties: { + alertThrottle: { + type: 'keyword', + }, + ruleAlertId: { + type: 'keyword', + }, + ruleThrottle: { + type: 'keyword', + }, + actions: { + properties: { + group: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + action_type_id: { + type: 'keyword', + }, + params: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dynamic: true as any, + properties: {}, }, }, }, }, }; + +export const type: SavedObjectsType = { + name: ruleActionsSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: ruleActionsSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index c23f539b58160..85b13ed9cf4ed 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -7,10 +7,10 @@ import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; -import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; +import { PatchRuleParams } from './types'; import { addTags } from './add_tags'; -import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion, calculateName, calculateInterval } from './utils'; +import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; export const patchRules = async ({ alertsClient, @@ -134,22 +134,22 @@ export const patchRules = async ({ await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); - const ruleCurrentStatus = savedObjectsClient - ? await savedObjectsClient.find({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }) - : null; + + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleCurrentStatus = await ruleStatusClient.find({ + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + // set current status for this rule to be 'going to run' if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - currentStatusToDisable.attributes.status = 'going to run'; - await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + await ruleStatusClient.update(currentStatusToDisable.id, { ...currentStatusToDisable.attributes, + status: 'going to run', }); } } else { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts index 1d91def5fa6cc..2dcc90240ad40 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts @@ -4,44 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsType } from '../../../../../../../src/core/server'; + export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status'; export const ruleStatusSavedObjectMappings = { - [ruleStatusSavedObjectType]: { - properties: { - alertId: { - type: 'keyword', - }, - status: { - type: 'keyword', - }, - statusDate: { - type: 'date', - }, - lastFailureAt: { - type: 'date', - }, - lastSuccessAt: { - type: 'date', - }, - lastFailureMessage: { - type: 'text', - }, - lastSuccessMessage: { - type: 'text', - }, - lastLookBackDate: { - type: 'date', - }, - gap: { - type: 'text', - }, - bulkCreateTimeDurations: { - type: 'float', - }, - searchAfterTimeDurations: { - type: 'float', - }, + properties: { + alertId: { + type: 'keyword', + }, + status: { + type: 'keyword', + }, + statusDate: { + type: 'date', + }, + lastFailureAt: { + type: 'date', + }, + lastSuccessAt: { + type: 'date', + }, + lastFailureMessage: { + type: 'text', + }, + lastSuccessMessage: { + type: 'text', + }, + lastLookBackDate: { + type: 'date', + }, + gap: { + type: 'text', + }, + bulkCreateTimeDurations: { + type: 'float', + }, + searchAfterTimeDurations: { + type: 'float', }, }, }; + +export const type: SavedObjectsType = { + name: ruleStatusSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: ruleStatusSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 7ddbbd76b0661..29c2cfdf91076 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -7,11 +7,11 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; -import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; +import { UpdateRuleParams } from './types'; import { addTags } from './add_tags'; -import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion } from './utils'; import { hasListsFeature } from '../feature_flags'; +import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; export const updateRules = async ({ alertsClient, @@ -129,22 +129,22 @@ export const updateRules = async ({ await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); - const ruleCurrentStatus = savedObjectsClient - ? await savedObjectsClient.find({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }) - : null; + + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleCurrentStatus = await ruleStatusClient.find({ + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + // set current status for this rule to be 'going to run' if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - currentStatusToDisable.attributes.status = 'going to run'; - await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + await ruleStatusClient.update(currentStatusToDisable.id, { ...currentStatusToDisable.attributes, + status: 'going to run', }); } } diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 8a5da8e859721..251a1e6d118ff 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -13,7 +13,7 @@ import { import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; import { RuleTypeParams, OutputRuleAlertRest } from '../../types'; import { IRuleStatusAttributes } from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../../../saved_objects'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; export const sampleRuleAlertParams = ( maxSignals?: number | undefined, @@ -86,7 +86,7 @@ export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSource _id: someUuid, _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', }, }); @@ -97,7 +97,7 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): Sig _id: someUuid, _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', }, }); @@ -109,7 +109,7 @@ export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSour _id: someUuid, _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', }, sort: ['1234567891111'], }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index bbd01cfaafc62..df9d282b71e5e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -59,7 +59,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], @@ -185,7 +185,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], @@ -309,7 +309,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], @@ -426,7 +426,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts index 0a50c33fbbfe4..f3f4ab60e4db6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts @@ -41,7 +41,7 @@ describe('buildSignal', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { created_by: 'elastic', @@ -101,7 +101,7 @@ describe('buildSignal', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', original_event: { action: 'socket_opened', dataset: 'socket', @@ -173,7 +173,7 @@ describe('buildSignal', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', original_event: { action: 'socket_opened', dataset: 'socket', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index cec011ae8c445..2cb23b05f6a9b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -30,7 +30,7 @@ describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { const sampleParams = sampleRuleAlertParams(); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults(), ruleParams: sampleParams, services: mockService, @@ -55,6 +55,7 @@ describe('searchAfterAndBulkCreate', () => { expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(0); + expect(lastLookBackDate).toBeNull(); }); test('if successful iteration of while loop with maxDocs', async () => { @@ -105,7 +106,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), ruleParams: sampleParams, services: mockService, @@ -130,13 +131,14 @@ describe('searchAfterAndBulkCreate', () => { expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(3); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if unsuccessful first bulk create', async () => { const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); const sampleParams = sampleRuleAlertParams(10); mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -161,6 +163,7 @@ describe('searchAfterAndBulkCreate', () => { expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { @@ -179,7 +182,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, @@ -204,6 +207,7 @@ describe('searchAfterAndBulkCreate', () => { expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { @@ -222,7 +226,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortIdNoHits(), ruleParams: sampleParams, services: mockService, @@ -246,6 +250,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { @@ -267,7 +272,7 @@ describe('searchAfterAndBulkCreate', () => { ], }) .mockResolvedValueOnce(sampleDocSearchResultsNoSortId()); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -291,6 +296,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { @@ -312,7 +318,7 @@ describe('searchAfterAndBulkCreate', () => { ], }) .mockResolvedValueOnce(sampleEmptyDocSearchResults()); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -336,6 +342,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if returns false when singleSearchAfter throws an exception', async () => { @@ -359,7 +366,7 @@ describe('searchAfterAndBulkCreate', () => { .mockImplementation(() => { throw Error('Fake Error'); }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -383,5 +390,6 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(false); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index e287e33295c89..acf3e9bfb055c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -98,13 +98,15 @@ export const searchAfterAndBulkCreate = async ({ tags, throttle, }); - toReturn.lastLookBackDate = - someResult.hits.hits.length > 0 - ? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp']) - : null; - if (createdItemsCount) { + + if (createdItemsCount > 0) { toReturn.createdSignalsCount = createdItemsCount; + toReturn.lastLookBackDate = + someResult.hits.hits.length > 0 + ? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp']) + : null; } + if (bulkCreateDuration) { toReturn.bulkCreateTimes.push(bulkCreateDuration); } diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 7eecc5cb9bad0..0c7f0839f8daf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; @@ -70,13 +70,13 @@ describe('rules_notification_alert_type', () => { }; let payload: jest.Mocked; let alert: ReturnType; - let logger: ReturnType; + let logger: ReturnType; let alertServices: AlertServicesMock; let ruleStatusService: Record; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); ruleStatusService = { success: jest.fn(), find: jest.fn(), diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 51cc0f449b17a..6f3cc6e708fce 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -300,7 +300,7 @@ describe('singleBulkCreate', () => { _id: 'e1e08ddc-5e37-49ff-a258-5393aa44435a', _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', signal: { parent: { rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', @@ -334,7 +334,7 @@ describe('singleBulkCreate', () => { test('filter duplicate rules will return back search responses if they do not have a signal and will NOT filter the source out', () => { const ancestors = sampleDocWithAncestors(); - ancestors.hits.hits[0]._source = { '@timestamp': 'some timestamp' }; + ancestors.hits.hits[0]._source = { '@timestamp': '2020-04-20T21:27:45+0000' }; const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', ancestors); expect(filtered).toEqual([ { @@ -343,7 +343,7 @@ describe('singleBulkCreate', () => { _score: 100, _version: 1, _id: 'e1e08ddc-5e37-49ff-a258-5393aa44435a', - _source: { '@timestamp': 'some timestamp' }, + _source: { '@timestamp': '2020-04-20T21:27:45+0000' }, }, ]); }); diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts index 2b94fd4516786..3eae30625e422 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object.ts @@ -25,9 +25,9 @@ import { import { FrameworkRequest } from '../framework'; import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; import { noteSavedObjectType } from './saved_object_mappings'; -import { timelineSavedObjectType } from '../../saved_objects'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; +import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; export class Note { public async deleteNote(request: FrameworkRequest, noteIds: string[]) { diff --git a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts index b001e30e52336..0f079571b868b 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts @@ -4,37 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedNote } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const noteSavedObjectType = 'siem-ui-timeline-note'; -export const noteSavedObjectMappings: { - [noteSavedObjectType]: ElasticsearchMappingOf; -} = { - [noteSavedObjectType]: { - properties: { - timelineId: { - type: 'keyword', - }, - eventId: { - type: 'keyword', - }, - note: { - type: 'text', - }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', - }, +export const noteSavedObjectMappings = { + properties: { + timelineId: { + type: 'keyword', + }, + eventId: { + type: 'keyword', + }, + note: { + type: 'text', + }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', }, }, }; + +export const type: SavedObjectsType = { + name: noteSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: noteSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts index 7fc23d86d8218..1e3a481e17106 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -20,9 +20,10 @@ import { SavedPinnedEvent, } from './types'; import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; -import { pinnedEventSavedObjectType, timelineSavedObjectType } from '../../saved_objects'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; +import { pinnedEventSavedObjectType } from './saved_object_mappings'; +import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; export class PinnedEvent { public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) { diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts index 322f585ae8ff2..1a4cd3fce575d 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts @@ -4,34 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedPinnedEvent } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event'; -export const pinnedEventSavedObjectMappings: { - [pinnedEventSavedObjectType]: ElasticsearchMappingOf; -} = { - [pinnedEventSavedObjectType]: { - properties: { - timelineId: { - type: 'keyword', - }, - eventId: { - type: 'keyword', - }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', - }, +export const pinnedEventSavedObjectMappings = { + properties: { + timelineId: { + type: 'keyword', + }, + eventId: { + type: 'keyword', + }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', }, }, }; + +export const type: SavedObjectsType = { + name: pinnedEventSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: pinnedEventSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts index 47ca25e16bd50..2bccb7c393837 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts @@ -85,7 +85,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value undefined supplied to : { ids: Array }/ids: Array' + 'Invalid value "undefined" supplied to "ids"' ); }); @@ -98,11 +98,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[1][0]).toEqual( - [ - 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/file_name: string', - 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/0: "true"', - 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/1: "false"', - ].join('\n') + 'Invalid value "undefined" supplied to "file_name",Invalid value "undefined" supplied to "exclude_export_details",Invalid value "undefined" supplied to "exclude_export_details"' ); }); }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts index c59f6eb6ce3da..e0eefbf811a56 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts @@ -8,7 +8,7 @@ import { set as _set } from 'lodash/fp'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; import { IRouter } from '../../../../../../../src/core/server'; -import { ConfigType } from '../../..'; +import { ConfigType } from '../../../config'; import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; import { getExportTimelineByObjectIds } from './utils/export_timelines'; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 3931bf0e5bea5..9f41943cfa27f 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -332,10 +332,7 @@ describe('import timelines', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - [ - 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/0: ReadableRt', - 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/1: { hapi: { filename: string } }', - ].join('\n') + 'Invalid value "undefined" supplied to "file",Invalid value "undefined" supplied to "file"' ); }); }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 258ef9faf671b..9d148abf82cdd 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -32,7 +32,7 @@ import { IRouter } from '../../../../../../../src/core/server'; import { SetupPlugins } from '../../../plugin'; import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; -import { ConfigType } from '../../..'; +import { ConfigType } from '../../../config'; import { Timeline } from '../saved_object'; import { validate } from '../../detection_engine/routes/rules/validate'; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index edd4abe0d76b5..677891fa16c02 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set as _set } from 'lodash/fp'; -import { - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, -} from '../../../../saved_objects'; import { NoteSavedObject } from '../../../note/types'; import { PinnedEventSavedObject } from '../../../pinned_event/types'; import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline'; @@ -30,6 +24,9 @@ import { TimelineSavedObject, } from '../../types'; import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; +import { pinnedEventSavedObjectType } from '../../../pinned_event/saved_object_mappings'; +import { noteSavedObjectType } from '../../../note/saved_object_mappings'; +import { timelineSavedObjectType } from '../../saved_object_mappings'; export type TimelineSavedObjectsClient = Pick< SavedObjectsClient, diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts index 8fc12fd56a8f6..b956e0f98fcb6 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -4,272 +4,274 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedTimeline } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const timelineSavedObjectType = 'siem-ui-timeline'; -export const timelineSavedObjectMappings: { - [timelineSavedObjectType]: ElasticsearchMappingOf; -} = { - [timelineSavedObjectType]: { - properties: { - columns: { - properties: { - aggregatable: { - type: 'boolean', - }, - category: { - type: 'keyword', - }, - columnHeaderType: { - type: 'keyword', - }, - description: { - type: 'text', - }, - example: { - type: 'text', - }, - indexes: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - placeholder: { - type: 'text', - }, - searchable: { - type: 'boolean', - }, - type: { - type: 'keyword', - }, +export const timelineSavedObjectMappings = { + properties: { + columns: { + properties: { + aggregatable: { + type: 'boolean', + }, + category: { + type: 'keyword', + }, + columnHeaderType: { + type: 'keyword', + }, + description: { + type: 'text', + }, + example: { + type: 'text', + }, + indexes: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + placeholder: { + type: 'text', + }, + searchable: { + type: 'boolean', + }, + type: { + type: 'keyword', }, }, - dataProviders: { - properties: { - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - enabled: { - type: 'boolean', - }, - excluded: { - type: 'boolean', - }, - kqlQuery: { - type: 'text', - }, - queryMatch: { - properties: { - field: { - type: 'text', - }, - displayField: { - type: 'text', - }, - value: { - type: 'text', - }, - displayValue: { - type: 'text', - }, - operator: { - type: 'text', - }, + }, + dataProviders: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + enabled: { + type: 'boolean', + }, + excluded: { + type: 'boolean', + }, + kqlQuery: { + type: 'text', + }, + queryMatch: { + properties: { + field: { + type: 'text', + }, + displayField: { + type: 'text', + }, + value: { + type: 'text', + }, + displayValue: { + type: 'text', + }, + operator: { + type: 'text', }, }, - and: { - properties: { - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - enabled: { - type: 'boolean', - }, - excluded: { - type: 'boolean', - }, - kqlQuery: { - type: 'text', - }, - queryMatch: { - properties: { - field: { - type: 'text', - }, - displayField: { - type: 'text', - }, - value: { - type: 'text', - }, - displayValue: { - type: 'text', - }, - operator: { - type: 'text', - }, + }, + and: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + enabled: { + type: 'boolean', + }, + excluded: { + type: 'boolean', + }, + kqlQuery: { + type: 'text', + }, + queryMatch: { + properties: { + field: { + type: 'text', + }, + displayField: { + type: 'text', + }, + value: { + type: 'text', + }, + displayValue: { + type: 'text', + }, + operator: { + type: 'text', }, }, }, }, }, }, - description: { - type: 'text', - }, - eventType: { - type: 'keyword', - }, - favorite: { - properties: { - keySearch: { - type: 'text', - }, - fullName: { - type: 'text', - }, - userName: { - type: 'text', - }, - favoriteDate: { - type: 'date', - }, + }, + description: { + type: 'text', + }, + eventType: { + type: 'keyword', + }, + favorite: { + properties: { + keySearch: { + type: 'text', + }, + fullName: { + type: 'text', + }, + userName: { + type: 'text', + }, + favoriteDate: { + type: 'date', }, }, - filters: { - properties: { - meta: { - properties: { - alias: { - type: 'text', - }, - controlledBy: { - type: 'text', - }, - disabled: { - type: 'boolean', - }, - field: { - type: 'text', - }, - formattedValue: { - type: 'text', - }, - index: { - type: 'keyword', - }, - key: { - type: 'keyword', - }, - negate: { - type: 'boolean', - }, - params: { - type: 'text', - }, - type: { - type: 'keyword', - }, - value: { - type: 'text', - }, + }, + filters: { + properties: { + meta: { + properties: { + alias: { + type: 'text', + }, + controlledBy: { + type: 'text', + }, + disabled: { + type: 'boolean', + }, + field: { + type: 'text', + }, + formattedValue: { + type: 'text', + }, + index: { + type: 'keyword', + }, + key: { + type: 'keyword', + }, + negate: { + type: 'boolean', + }, + params: { + type: 'text', + }, + type: { + type: 'keyword', + }, + value: { + type: 'text', }, - }, - exists: { - type: 'text', - }, - match_all: { - type: 'text', - }, - missing: { - type: 'text', - }, - query: { - type: 'text', - }, - range: { - type: 'text', - }, - script: { - type: 'text', }, }, + exists: { + type: 'text', + }, + match_all: { + type: 'text', + }, + missing: { + type: 'text', + }, + query: { + type: 'text', + }, + range: { + type: 'text', + }, + script: { + type: 'text', + }, }, - kqlMode: { - type: 'keyword', - }, - kqlQuery: { - properties: { - filterQuery: { - properties: { - kuery: { - properties: { - kind: { - type: 'keyword', - }, - expression: { - type: 'text', - }, + }, + kqlMode: { + type: 'keyword', + }, + kqlQuery: { + properties: { + filterQuery: { + properties: { + kuery: { + properties: { + kind: { + type: 'keyword', + }, + expression: { + type: 'text', }, }, - serializedQuery: { - type: 'text', - }, + }, + serializedQuery: { + type: 'text', }, }, }, }, - title: { - type: 'text', - }, - dateRange: { - properties: { - start: { - type: 'date', - }, - end: { - type: 'date', - }, + }, + title: { + type: 'text', + }, + dateRange: { + properties: { + start: { + type: 'date', }, - }, - savedQueryId: { - type: 'keyword', - }, - sort: { - properties: { - columnId: { - type: 'keyword', - }, - sortDirection: { - type: 'keyword', - }, + end: { + type: 'date', }, }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', + }, + savedQueryId: { + type: 'keyword', + }, + sort: { + properties: { + columnId: { + type: 'keyword', + }, + sortDirection: { + type: 'keyword', + }, }, }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', + }, }, }; + +export const type: SavedObjectsType = { + name: timelineSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: timelineSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/types.ts b/x-pack/plugins/siem/server/lib/types.ts index a74fe8f778ba9..2a897806dc628 100644 --- a/x-pack/plugins/siem/server/lib/types.ts +++ b/x-pack/plugins/siem/server/lib/types.ts @@ -6,7 +6,7 @@ import { AuthenticatedUser } from '../../../security/public'; import { RequestHandlerContext } from '../../../../../src/core/server'; -export { ConfigType as Configuration } from '../'; +export { ConfigType as Configuration } from '../config'; import { Authentications } from './authentications'; import { Events } from './events'; diff --git a/x-pack/plugins/siem/server/mocks.ts b/x-pack/plugins/siem/server/mocks.ts new file mode 100644 index 0000000000000..44c41be86b6ff --- /dev/null +++ b/x-pack/plugins/siem/server/mocks.ts @@ -0,0 +1,17 @@ +/* + * 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 { SiemClient } from './types'; + +type SiemClientMock = jest.Mocked; +const createSiemClientMock = (): SiemClientMock => + (({ + getSignalsIndex: jest.fn(), + } as unknown) as SiemClientMock); + +export const siemMock = { + createClient: createSiemClientMock, +}; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index b9ec1c2e92438..3988fbec05de4 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, + Plugin as IPlugin, PluginInitializerContext, Logger, } from '../../../../src/core/server'; @@ -33,15 +34,10 @@ import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type'; import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types'; import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; -import { - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, -} from './saved_objects'; +import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { SiemClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; +import { initUiSettings } from './ui_settings'; export { CoreSetup, CoreStart }; @@ -60,7 +56,12 @@ export interface StartPlugins { alerting: AlertingStart; } -export class Plugin { +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginStart {} + +export class Plugin implements IPlugin { readonly name = 'siem'; private readonly logger: Logger; private readonly config$: Observable; @@ -86,6 +87,9 @@ export class Plugin { ); } + initSavedObjects(core.savedObjects); + initUiSettings(core.uiSettings); + const router = core.http.createRouter(); core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({ getSiemClient: () => this.siemClientFactory.create(request), @@ -125,15 +129,11 @@ export class Plugin { 'alert', 'action', 'action_task_params', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, 'cases', 'cases-comments', 'cases-configure', 'cases-user-actions', + ...savedObjectTypes, ], read: ['config'], }, @@ -156,15 +156,11 @@ export class Plugin { all: ['alert', 'action', 'action_task_params'], read: [ 'config', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, 'cases', 'cases-comments', 'cases-configure', 'cases-user-actions', + ...savedObjectTypes, ], }, ui: [ @@ -201,7 +197,11 @@ export class Plugin { const libs = compose(core, plugins, this.context.env.mode.prod); initServer(libs); + + return {}; } - public start(core: CoreStart, plugins: StartPlugins) {} + public start(core: CoreStart, plugins: StartPlugins) { + return {}; + } } diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts index 64b232a2686b8..1c03823e85fd7 100644 --- a/x-pack/plugins/siem/server/routes/index.ts +++ b/x-pack/plugins/siem/server/routes/index.ts @@ -31,7 +31,7 @@ import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/r import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route'; import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route'; import { SetupPlugins } from '../plugin'; -import { ConfigType } from '..'; +import { ConfigType } from '../config'; export const initRoutes = ( router: IRouter, diff --git a/x-pack/plugins/siem/server/saved_objects.ts b/x-pack/plugins/siem/server/saved_objects.ts index 7b097eefedb46..66a470099d649 100644 --- a/x-pack/plugins/siem/server/saved_objects.ts +++ b/x-pack/plugins/siem/server/saved_objects.ts @@ -4,35 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noteSavedObjectType, noteSavedObjectMappings } from './lib/note/saved_object_mappings'; -import { - pinnedEventSavedObjectType, - pinnedEventSavedObjectMappings, -} from './lib/pinned_event/saved_object_mappings'; -import { - timelineSavedObjectType, - timelineSavedObjectMappings, -} from './lib/timeline/saved_object_mappings'; -import { - ruleStatusSavedObjectMappings, - ruleStatusSavedObjectType, -} from './lib/detection_engine/rules/saved_object_mappings'; -import { - ruleActionsSavedObjectMappings, - ruleActionsSavedObjectType, -} from './lib/detection_engine/rule_actions/saved_object_mappings'; +import { CoreSetup } from '../../../../src/core/server'; -export { - noteSavedObjectType, - pinnedEventSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, - timelineSavedObjectType, -}; -export const savedObjectMappings = { - ...timelineSavedObjectMappings, - ...noteSavedObjectMappings, - ...pinnedEventSavedObjectMappings, - ...ruleStatusSavedObjectMappings, - ...ruleActionsSavedObjectMappings, +import { type as noteType } from './lib/note/saved_object_mappings'; +import { type as pinnedEventType } from './lib/pinned_event/saved_object_mappings'; +import { type as timelineType } from './lib/timeline/saved_object_mappings'; +import { type as ruleStatusType } from './lib/detection_engine/rules/saved_object_mappings'; +import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings'; + +const types = [noteType, pinnedEventType, ruleActionsType, ruleStatusType, timelineType]; + +export const savedObjectTypes = types.map(type => type.name); + +export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => { + types.forEach(type => savedObjects.registerType(type)); }; diff --git a/x-pack/plugins/siem/server/ui_settings.ts b/x-pack/plugins/siem/server/ui_settings.ts new file mode 100644 index 0000000000000..26b7fd72571af --- /dev/null +++ b/x-pack/plugins/siem/server/ui_settings.ts @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { CoreSetup } from '../../../../src/core/server'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_INDEX_PATTERN, + DEFAULT_ANOMALY_SCORE, + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_FROM, + DEFAULT_TO, + ENABLE_NEWS_FEED_SETTING, + NEWS_FEED_URL_SETTING, + NEWS_FEED_URL_SETTING_DEFAULT, + IP_REPUTATION_LINKS_SETTING, + IP_REPUTATION_LINKS_SETTING_DEFAULT, +} from '../common/constants'; + +export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { + uiSettings.register({ + [DEFAULT_SIEM_REFRESH_INTERVAL]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', { + defaultMessage: 'Time filter refresh interval', + }), + value: `{ + "pause": ${DEFAULT_INTERVAL_PAUSE}, + "value": ${DEFAULT_INTERVAL_VALUE} +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', { + defaultMessage: + '

Default refresh interval for the SIEM time filter, in milliseconds.

', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.object({ + value: schema.number(), + pause: schema.boolean(), + }), + }, + [DEFAULT_SIEM_TIME_RANGE]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', { + defaultMessage: 'Time filter period', + }), + value: `{ + "from": "${DEFAULT_FROM}", + "to": "${DEFAULT_TO}" +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', { + defaultMessage: '

Default period of time in the SIEM time filter.

', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + }, + [DEFAULT_INDEX_KEY]: { + name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { + defaultMessage: 'Elasticsearch indices', + }), + value: DEFAULT_INDEX_PATTERN, + description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', { + defaultMessage: + '

Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.

', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.arrayOf(schema.string()), + }, + [DEFAULT_ANOMALY_SCORE]: { + name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', { + defaultMessage: 'Anomaly threshold', + }), + value: 50, + type: 'number', + description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', { + defaultMessage: + '

Value above which Machine Learning job anomalies are displayed in the SIEM app.

Valid values: 0 to 100.

', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.number(), + }, + [ENABLE_NEWS_FEED_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', { + defaultMessage: 'News feed', + }), + value: true, + description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', { + defaultMessage: '

Enables the News feed

', + }), + type: 'boolean', + category: ['siem'], + requiresPageReload: true, + schema: schema.boolean(), + }, + [NEWS_FEED_URL_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', { + defaultMessage: 'News feed URL', + }), + value: NEWS_FEED_URL_SETTING_DEFAULT, + description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', { + defaultMessage: '

News feed content will be retrieved from this URL

', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.string(), + }, + [IP_REPUTATION_LINKS_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', { + defaultMessage: 'IP Reputation Links', + }), + value: IP_REPUTATION_LINKS_SETTING_DEFAULT, + type: 'json', + description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', { + defaultMessage: + 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.arrayOf( + schema.object({ + name: schema.string(), + url_template: schema.string(), + }) + ), + }, + }); +}; diff --git a/x-pack/plugins/siem/server/utils/build_validation/__mocks__/utils.ts b/x-pack/plugins/siem/server/utils/build_validation/__mocks__/utils.ts new file mode 100644 index 0000000000000..578972dda5aef --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/__mocks__/utils.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { formatErrors } from '../format_errors'; + +interface Message { + errors: t.Errors; + schema: T | {}; +} + +const onLeft = (errors: t.Errors): Message => { + return { schema: {}, errors }; +}; + +const onRight = (schema: T): Message => { + return { + schema, + errors: [], + }; +}; + +export const foldLeftRight = fold(onLeft, onRight); + +/** + * Convenience utility to keep the error message handling within tests to be + * very concise. + * @param validation The validation to get the errors from + */ +export const getPaths =
(validation: t.Validation): string[] => { + return pipe( + validation, + fold( + errors => formatErrors(errors), + () => ['no errors'] + ) + ); +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts b/x-pack/plugins/siem/server/utils/build_validation/exact_check.test.ts similarity index 94% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts rename to x-pack/plugins/siem/server/utils/build_validation/exact_check.test.ts index cae4365d06856..1e70deaeed438 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/exact_check.test.ts @@ -5,22 +5,13 @@ */ import * as t from 'io-ts'; -import { left, right } from 'fp-ts/lib/Either'; +import { left, right, Either } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { exactCheck, findDifferencesRecursive } from './exact_check'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('exact_check', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it returns an error if given extra object properties', () => { const someType = t.exact( t.type({ @@ -36,14 +27,22 @@ describe('exact_check', () => { }); test('it returns an error if the data type is not as expected', () => { + type UnsafeCastForTest = Either< + t.Errors, + { + a: number; + } + >; + const someType = t.exact( t.type({ a: t.string, }) ); + const payload = { a: 1 }; const decoded = someType.decode(payload); - const checked = exactCheck(payload, decoded); + const checked = exactCheck(payload, decoded as UnsafeCastForTest); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']); expect(message.schema).toEqual({}); diff --git a/x-pack/plugins/siem/server/utils/build_validation/exact_check.ts b/x-pack/plugins/siem/server/utils/build_validation/exact_check.ts new file mode 100644 index 0000000000000..9484765f9973d --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/exact_check.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { left, Either, fold, right } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { isObject, get } from 'lodash/fp'; + +/** + * Given an original object and a decoded object this will return an error + * if and only if the original object has additional keys that the decoded + * object does not have. If the original decoded already has an error, then + * this will return the error as is and not continue. + * + * NOTE: You MUST use t.exact(...) for this to operate correctly as your schema + * needs to remove additional keys before the compare + * + * You might not need this in the future if the below issue is solved: + * https://github.com/gcanti/io-ts/issues/322 + * + * @param original The original to check if it has additional keys + * @param decoded The decoded either which has either an existing error or the + * decoded object which could have additional keys stripped from it. + */ +export const exactCheck = (original: T, decoded: Either): Either => { + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = (decodedValue: T): Either => { + const differences = findDifferencesRecursive(original, decodedValue); + if (differences.length !== 0) { + const validationError: t.ValidationError = { + value: differences, + context: [], + message: `invalid keys "${differences.join(',')}"`, + }; + const error: t.Errors = [validationError]; + return left(error); + } else { + return right(decodedValue); + } + }; + return pipe(decoded, fold(onLeft, onRight)); +}; + +export const findDifferencesRecursive = (original: T, decodedValue: T): string[] => { + if (decodedValue == null) { + try { + // It is null and painful when the original contains an object or an array + // the the decoded value does not have. + return [JSON.stringify(original)]; + } catch (err) { + return ['circular reference']; + } + } else if (typeof original !== 'object' || original == null) { + // We are not an object or null so do not report differences + return []; + } else { + const decodedKeys = Object.keys(decodedValue); + const differences = Object.keys(original).flatMap(originalKey => { + const foundKey = decodedKeys.some(key => key === originalKey); + const topLevelKey = foundKey ? [] : [originalKey]; + // I use lodash to cheat and get an any (not going to lie ;-)) + const valueObjectOrArrayOriginal = get(originalKey, original); + const valueObjectOrArrayDecoded = get(originalKey, decodedValue); + if (isObject(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), + ]; + } else if (Array.isArray(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => + findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) + ), + ]; + } else { + return topLevelKey; + } + }); + return differences; + } +}; diff --git a/x-pack/plugins/siem/server/utils/build_validation/format_errors.test.ts b/x-pack/plugins/siem/server/utils/build_validation/format_errors.test.ts new file mode 100644 index 0000000000000..f9dd9e76a1d9c --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/format_errors.test.ts @@ -0,0 +1,130 @@ +/* + * 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 * as t from 'io-ts'; +import { formatErrors } from './format_errors'; + +describe('utils', () => { + test('returns an empty error message string if there are no errors', () => { + const errors: t.Errors = []; + const output = formatErrors(errors); + expect(output).toEqual([]); + }); + + test('returns a single error message if given one', () => { + const validationError: t.ValidationError = { + value: 'Some existing error', + context: [], + message: 'some error', + }; + const errors: t.Errors = [validationError]; + const output = formatErrors(errors); + expect(output).toEqual(['some error']); + }); + + test('returns a two error messages if given two', () => { + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const validationError2: t.ValidationError = { + value: 'Some existing error 2', + context: [], + message: 'some error 2', + }; + const errors: t.Errors = [validationError1, validationError2]; + const output = formatErrors(errors); + expect(output).toEqual(['some error 1', 'some error 2']); + }); + + test('will use message before context if it is set', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + message: 'I should be used first', + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['I should be used first']); + }); + + test('will use context entry of a single string', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); + }); + + test('will use two context entries of two strings', () => { + const context: t.Context = ([ + { key: 'some string key 1' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', + ]); + }); + + test('will filter out and not use any strings of numbers', () => { + const context: t.Context = ([ + { key: '5' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use null', () => { + const context: t.Context = ([ + { key: null }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use empty strings', () => { + const context: t.Context = ([ + { key: '' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts b/x-pack/plugins/siem/server/utils/build_validation/format_errors.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts rename to x-pack/plugins/siem/server/utils/build_validation/format_errors.ts diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts index d17a8457ff81b..8a1e4271303a6 100644 --- a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts @@ -6,12 +6,36 @@ import { buildRouteValidation } from './route_validation'; import * as rt from 'io-ts'; -import { RouteValidationResultFactory } from '../../../../../../src/core/server/http'; +import { RouteValidationResultFactory } from 'src/core/server'; describe('buildRouteValidation', () => { - const schema = rt.type({ - ids: rt.array(rt.string), - }); + const schema = rt.exact( + rt.type({ + ids: rt.array(rt.string), + }) + ); + type Schema = rt.TypeOf; + + /** + * If your schema is using exact all the way down then the validation will + * catch any additional keys that should not be present within the validation + * when the route_validation uses the exact check. + */ + const deepSchema = rt.exact( + rt.type({ + topLevel: rt.exact( + rt.type({ + secondLevel: rt.exact( + rt.type({ + thirdLevel: rt.string, + }) + ), + }) + ), + }) + ); + type DeepSchema = rt.TypeOf; + const validationResult: RouteValidationResultFactory = { ok: jest.fn().mockImplementation(validatedInput => validatedInput), badRequest: jest.fn().mockImplementation(e => e), @@ -22,18 +46,41 @@ describe('buildRouteValidation', () => { }); test('return validation error', () => { - const input = { id: 'someId' }; + const input: Omit & { id: string } = { id: 'someId' }; const result = buildRouteValidation(schema)(input, validationResult); - expect(result).toEqual( - 'Invalid value undefined supplied to : { ids: Array }/ids: Array' - ); + expect(result).toEqual('Invalid value "undefined" supplied to "ids"'); }); test('return validated input', () => { - const input = { ids: ['someId'] }; + const input: Schema = { ids: ['someId'] }; const result = buildRouteValidation(schema)(input, validationResult); expect(result).toEqual(input); }); + + test('returns validation error if given extra keys on input for an array', () => { + const input: Schema & { somethingExtra: string } = { + ids: ['someId'], + somethingExtra: 'hello', + }; + const result = buildRouteValidation(schema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingExtra"'); + }); + + test('return validation input for a deep 3rd level object', () => { + const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual(input); + }); + + test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { + const input: DeepSchema & { + topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; + } = { + topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingElse"'); + }); }); diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts index bfcd0998fe690..30b95dcfa94ee 100644 --- a/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts @@ -7,12 +7,13 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; import { RouteValidationFunction, RouteValidationResultFactory, RouteValidationError, } from '../../../../../../src/core/server'; +import { exactCheck } from './exact_check'; +import { formatErrors } from './format_errors'; type RequestValidationResult = | { @@ -32,8 +33,9 @@ export const buildRouteValidation = >( ) => pipe( schema.decode(inputValue), + decoded => exactCheck(inputValue, decoded), fold>( - (errors: rt.Errors) => validationResult.badRequest(failure(errors).join('\n')), + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), (validatedInput: A) => validationResult.ok(validatedInput) ) ); diff --git a/x-pack/plugins/snapshot_restore/common/types/index.ts b/x-pack/plugins/snapshot_restore/common/types/index.ts index 5cb3839fa9e01..d52584ca737a2 100644 --- a/x-pack/plugins/snapshot_restore/common/types/index.ts +++ b/x-pack/plugins/snapshot_restore/common/types/index.ts @@ -8,4 +8,3 @@ export * from './repository'; export * from './snapshot'; export * from './restore'; export * from './policy'; -export * from './privileges'; diff --git a/x-pack/plugins/snapshot_restore/common/types/privileges.ts b/x-pack/plugins/snapshot_restore/common/types/privileges.ts deleted file mode 100644 index bf710b8225599..0000000000000 --- a/x-pack/plugins/snapshot_restore/common/types/privileges.ts +++ /dev/null @@ -1,14 +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. - */ - -export interface MissingPrivileges { - [key: string]: string[] | undefined; -} - -export interface Privileges { - hasAllPrivileges: boolean; - missingPrivileges: MissingPrivileges; -} diff --git a/x-pack/plugins/snapshot_restore/public/application/app.tsx b/x-pack/plugins/snapshot_restore/public/application/app.tsx index 77ef697814b2c..350d8aec711ed 100644 --- a/x-pack/plugins/snapshot_restore/public/application/app.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/app.tsx @@ -4,13 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common/constants'; -import { SectionLoading, SectionError } from './components'; +import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common'; +import { + useAuthorizationContext, + SectionError, + WithPrivileges, + NotAuthorizedSection, +} from '../shared_imports'; +import { SectionLoading } from './components'; import { BASE_PATH, DEFAULT_SECTION, Section } from './constants'; import { RepositoryAdd, @@ -21,11 +27,10 @@ import { PolicyEdit, } from './sections'; import { useConfig } from './app_context'; -import { AuthorizationContext, WithPrivileges, NotAuthorizedSection } from './lib/authorization'; export const App: React.FunctionComponent = () => { const { slm_ui: slmUi } = useConfig(); - const { apiError } = useContext(AuthorizationContext); + const { apiError } = useAuthorizationContext(); const sections: Section[] = ['repositories', 'snapshots', 'restore_status']; diff --git a/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx b/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx index e2732c0051337..3ca25b7d32dba 100644 --- a/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { API_BASE_PATH } from '../../common/constants'; -import { AuthorizationProvider } from './lib/authorization'; +import { API_BASE_PATH } from '../../common'; +import { AuthorizationProvider } from '../shared_imports'; import { AppContextProvider, AppDependencies } from './app_context'; interface Props { @@ -18,10 +18,11 @@ export const AppProviders = ({ appDependencies, children }: Props) => { const { core } = appDependencies; const { i18n: { Context: I18nContext }, + http, } = core; return ( - + {children} diff --git a/x-pack/plugins/snapshot_restore/public/application/components/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/index.ts index a7038ebd71578..f5bb892389870 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/index.ts @@ -10,7 +10,6 @@ export { RepositoryDeleteProvider } from './repository_delete_provider'; export { RepositoryForm } from './repository_form'; export { RepositoryVerificationBadge } from './repository_verification_badge'; export { RepositoryTypeLogo } from './repository_type_logo'; -export { SectionError, Error } from './section_error'; export { SectionLoading } from './section_loading'; export { SnapshotDeleteProvider } from './snapshot_delete_provider'; export { RestoreSnapshotForm } from './restore_snapshot_form'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index f2d4e2bd74598..105f0601e3dfb 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -21,13 +21,13 @@ import { } from '@elastic/eui'; import { Repository } from '../../../../../common/types'; -import { CronEditor } from '../../../../shared_imports'; +import { CronEditor, SectionError } from '../../../../shared_imports'; import { useServices } from '../../../app_context'; import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants'; import { useLoadRepositories } from '../../../services/http'; import { linkToAddRepository } from '../../../services/navigation'; import { documentationLinksService } from '../../../services/documentation'; -import { SectionLoading, SectionError } from '../../'; +import { SectionLoading } from '../../'; import { StepProps } from './'; export const PolicyStepLogistics: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx index 3b4c9d595b9f2..34bc06343a780 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx @@ -23,13 +23,14 @@ import { } from '@elastic/eui'; import { Repository, RepositoryType, EmptyRepository } from '../../../../common/types'; -import { REPOSITORY_TYPES } from '../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../common'; +import { SectionError, Error } from '../../../shared_imports'; import { documentationLinksService } from '../../services/documentation'; import { useLoadRepositoryTypes } from '../../services/http'; import { textService } from '../../services/text'; import { RepositoryValidation } from '../../services/validation'; -import { SectionError, SectionLoading, RepositoryTypeLogo, Error } from '../'; +import { SectionLoading, RepositoryTypeLogo } from '../'; interface Props { repository: Repository | EmptyRepository; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx index 75295a1205cef..1e54868397a6d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx @@ -6,11 +6,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { REPOSITORY_TYPES } from '../../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../../common'; import { Repository, RepositoryType, EmptyRepository } from '../../../../../common/types'; +import { SectionError } from '../../../../shared_imports'; import { useServices } from '../../../app_context'; import { RepositorySettingsValidation } from '../../../services/validation'; -import { SectionError } from '../../index'; import { AzureSettings } from './azure_settings'; import { FSSettings } from './fs_settings'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/section_error.tsx b/x-pack/plugins/snapshot_restore/public/application/components/section_error.tsx deleted file mode 100644 index bd9e48796779e..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/components/section_error.tsx +++ /dev/null @@ -1,50 +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 { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { Fragment } from 'react'; - -export interface Error { - error: string; - cause?: string[]; - message?: string; -} - -interface Props { - title: React.ReactNode; - error: Error; - actions?: JSX.Element; -} - -export const SectionError: React.FunctionComponent = ({ - title, - error, - actions, - ...rest -}) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error; - - return ( - - {cause ? message || errorString :

{message || errorString}

} - {cause && ( - - -
    - {cause.map((causeMsg, i) => ( -
  • {causeMsg}
  • - ))} -
-
- )} - {actions ? actions : null} -
- ); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/authorization_provider.tsx deleted file mode 100644 index d32fe29cc1dfa..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/authorization_provider.tsx +++ /dev/null @@ -1,47 +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 React, { createContext } from 'react'; -import { useRequest } from '../../../services/http/use_request'; -import { Privileges } from '../../../../../common/types'; -import { Error } from '../../../components/section_error'; - -interface Authorization { - isLoading: boolean; - apiError: Error | null; - privileges: Privileges; -} - -const initialValue: Authorization = { - isLoading: true, - apiError: null, - privileges: { - hasAllPrivileges: true, - missingPrivileges: {}, - }, -}; - -export const AuthorizationContext = createContext(initialValue); - -interface Props { - privilegesEndpoint: string; - children: React.ReactNode; -} - -export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => { - const { isLoading, error, data: privilegesData } = useRequest({ - path: privilegesEndpoint, - method: 'get', - }); - - const value = { - isLoading, - privileges: isLoading ? { hasAllPrivileges: true, missingPrivileges: {} } : privilegesData, - apiError: error ? error : null, - } as Authorization; - - return {children}; -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/index.ts b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/index.ts deleted file mode 100644 index ac77aa5268660..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/index.ts +++ /dev/null @@ -1,11 +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. - */ - -export { AuthorizationProvider, AuthorizationContext } from './authorization_provider'; - -export { WithPrivileges } from './with_privileges'; - -export { NotAuthorizedSection } from './not_authorized_section'; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/not_authorized_section.tsx b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/not_authorized_section.tsx deleted file mode 100644 index 3fc13245708e8..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/not_authorized_section.tsx +++ /dev/null @@ -1,17 +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 React from 'react'; -import { EuiEmptyPrompt } from '@elastic/eui'; - -interface Props { - title: React.ReactNode; - message: React.ReactNode | string; -} - -export const NotAuthorizedSection = ({ title, message }: Props) => ( - {title}} body={

{message}

} /> -); diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/with_privileges.tsx deleted file mode 100644 index 223a2882c3cab..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/with_privileges.tsx +++ /dev/null @@ -1,78 +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 { useContext } from 'react'; - -import { MissingPrivileges } from '../../../../../common/types'; -import { AuthorizationContext } from './authorization_provider'; - -interface Props { - /** - * Each required privilege must have the format "section.privilege". - * To indicate that *all* privileges from a section are required, we can use the asterix - * e.g. "index.*" - */ - privileges: string | string[]; - children: (childrenProps: { - isLoading: boolean; - hasPrivileges: boolean; - privilegesMissing: MissingPrivileges; - }) => JSX.Element; -} - -type Privilege = [string, string]; - -const toArray = (value: string | string[]): string[] => - Array.isArray(value) ? (value as string[]) : ([value] as string[]); - -export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { - const { isLoading, privileges } = useContext(AuthorizationContext); - - const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map(p => { - const [section, privilege] = p.split('.'); - if (!privilege) { - // Oh! we forgot to use the dot "." notation. - throw new Error('Required privilege must have the format "section.privilege"'); - } - return [section, privilege]; - }); - - const hasPrivileges = isLoading - ? false - : privilegesToArray.every(privilege => { - const [section, requiredPrivilege] = privilege; - if (!privileges.missingPrivileges[section]) { - // if the section does not exist in our missingPriviledges, everything is OK - return true; - } - if (privileges.missingPrivileges[section]!.length === 0) { - return true; - } - if (requiredPrivilege === '*') { - // If length > 0 and we require them all... KO - return false; - } - // If we require _some_ privilege, we make sure that the one - // we require is *not* in the missingPrivilege array - return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); - }); - - const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { - if (privilege === '*') { - acc[section] = privileges.missingPrivileges[section] || []; - } else if ( - privileges.missingPrivileges[section] && - privileges.missingPrivileges[section]!.includes(privilege) - ) { - const missing: string[] = acc[section] || []; - acc[section] = [...missing, privilege]; - } - - return acc; - }, {} as MissingPrivileges); - - return children({ isLoading, hasPrivileges, privilegesMissing }); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/index.ts b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/index.ts deleted file mode 100644 index 73bbde465146c..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './components'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx index f3110199ee17c..03d381c9f3aa3 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx @@ -26,6 +26,7 @@ import { import { SlmPolicy } from '../../../../../../common/types'; import { useServices } from '../../../../app_context'; +import { SectionError, Error } from '../../../../../shared_imports'; import { UIM_POLICY_DETAIL_PANEL_SUMMARY_TAB, UIM_POLICY_DETAIL_PANEL_HISTORY_TAB, @@ -34,11 +35,9 @@ import { useLoadPolicy } from '../../../../services/http'; import { linkToEditPolicy, linkToSnapshot } from '../../../../services/navigation'; import { - SectionError, SectionLoading, PolicyExecuteProvider, PolicyDeleteProvider, - Error, } from '../../../../components'; import { TabSummary, TabHistory } from './tabs'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx index 0122e25e5e165..51297038b0f3f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx @@ -9,13 +9,19 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiEmptyPrompt, EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { + SectionError, + Error, + WithPrivileges, + NotAuthorizedSection, +} from '../../../../shared_imports'; + import { SlmPolicy } from '../../../../../common/types'; -import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common'; +import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants'; import { useLoadPolicies, useLoadRetentionSettings } from '../../../services/http'; import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation'; -import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization'; import { useServices } from '../../../app_context'; import { PolicyDetails } from './policy_details'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx index 7f9c5c5af7705..ba28bcddf5347 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; +import { Error } from '../../../../../shared_imports'; import { UIM_POLICY_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useServices } from '../../../../app_context'; import { @@ -28,7 +29,6 @@ import { PolicyExecuteProvider, PolicyDeleteProvider, } from '../../../../components'; -import { Error } from '../../../../components/section_error'; import { linkToAddPolicy, linkToEditPolicy } from '../../../../services/navigation'; import { SendRequestResponse } from '../../../../../shared_imports'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx index d293f194f647a..9932f14664076 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx @@ -25,6 +25,8 @@ import { import 'brace/theme/textmate'; +import { SectionError, Error } from '../../../../../shared_imports'; + import { useServices } from '../../../../app_context'; import { documentationLinksService } from '../../../../services/documentation'; import { @@ -35,7 +37,8 @@ import { import { textService } from '../../../../services/text'; import { linkToSnapshots, linkToEditRepository } from '../../../../services/navigation'; -import { REPOSITORY_TYPES } from '../../../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../../../common'; + import { Repository, RepositoryVerification, @@ -43,10 +46,8 @@ import { } from '../../../../../../common/types'; import { RepositoryDeleteProvider, - SectionError, SectionLoading, RepositoryVerificationBadge, - Error, } from '../../../../components'; import { TypeDetails } from './type_details'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx index 6fa12537e9d6f..2256fa5991dec 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx @@ -10,7 +10,8 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { Repository } from '../../../../../common/types'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { SectionError, Error } from '../../../../shared_imports'; +import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_REPOSITORY_LIST_LOAD } from '../../../constants'; import { useServices } from '../../../app_context'; import { useLoadRepositories } from '../../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx index 7c0438f6b837f..bf2643d78bca1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx @@ -15,15 +15,14 @@ import { EuiIconTip, } from '@elastic/eui'; -import { REPOSITORY_TYPES } from '../../../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../../../common'; import { Repository, RepositoryType } from '../../../../../../common/types'; -import { Error } from '../../../../components/section_error'; +import { Error, SendRequestResponse } from '../../../../../shared_imports'; import { RepositoryDeleteProvider } from '../../../../components'; import { UIM_REPOSITORY_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useServices } from '../../../../app_context'; import { textService } from '../../../../services/text'; import { linkToEditRepository, linkToAddRepository } from '../../../../services/navigation'; -import { SendRequestResponse } from '../../../../../shared_imports'; interface Props { repositories: Repository[]; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx index da9ce3b124a11..0e3d9363d0535 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx @@ -18,14 +18,19 @@ import { EuiLoadingSpinner, EuiLink, } from '@elastic/eui'; -import { APP_RESTORE_INDEX_PRIVILEGES } from '../../../../../common/constants'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { APP_RESTORE_INDEX_PRIVILEGES } from '../../../../../common'; +import { + WithPrivileges, + NotAuthorizedSection, + SectionError, + Error, +} from '../../../../shared_imports'; +import { SectionLoading } from '../../../components'; import { UIM_RESTORE_LIST_LOAD } from '../../../constants'; import { useLoadRestores } from '../../../services/http'; import { linkToSnapshots } from '../../../services/navigation'; import { useServices } from '../../../app_context'; import { RestoreTable } from './restore_table'; -import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization'; const ONE_SECOND_MS = 1000; const TEN_SECONDS_MS = 10 * 1000; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx index d16545debe1ec..1943762a3c36e 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx @@ -23,12 +23,8 @@ import React, { Fragment, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { SnapshotDetails as ISnapshotDetails } from '../../../../../../common/types'; -import { - SectionError, - SectionLoading, - SnapshotDeleteProvider, - Error, -} from '../../../../components'; +import { SectionError, Error } from '../../../../../shared_imports'; +import { SectionLoading, SnapshotDeleteProvider } from '../../../../components'; import { useServices } from '../../../../app_context'; import { UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx index fe99ccb6f596c..30e4c771644bc 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx @@ -10,10 +10,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiCallOut, EuiLink, EuiEmptyPrompt, EuiSpacer, EuiIcon } from '@elastic/eui'; -import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common'; +import { WithPrivileges, SectionError, Error } from '../../../../shared_imports'; +import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants'; -import { WithPrivileges } from '../../../lib/authorization'; import { documentationLinksService } from '../../../services/documentation'; import { useLoadSnapshots } from '../../../services/http'; import { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx index ad64dcc7adcfe..427c241970007 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx @@ -17,10 +17,10 @@ import { } from '@elastic/eui'; import { SnapshotDetails } from '../../../../../../common/types'; +import { Error } from '../../../../../shared_imports'; import { SNAPSHOT_STATE, UIM_SNAPSHOT_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useServices } from '../../../../app_context'; import { linkToRepository, linkToRestoreSnapshot } from '../../../../services/navigation'; -import { Error } from '../../../../components/section_error'; import { DataPlaceholder, FormattedDateTime, SnapshotDeleteProvider } from '../../../../components'; import { SendRequestResponse } from '../../../../../shared_imports'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx index 4eb0f54978d09..6d1a432be7f9f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx @@ -9,9 +9,11 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../common/types'; -import { TIME_UNITS } from '../../../../common/constants'; +import { TIME_UNITS } from '../../../../common'; -import { PolicyForm, SectionError, SectionLoading, Error } from '../../components'; +import { SectionError, Error } from '../../../shared_imports'; + +import { PolicyForm, SectionLoading } from '../../components'; import { BASE_PATH, DEFAULT_POLICY_SCHEDULE } from '../../constants'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { addPolicy, useLoadIndices } from '../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx index 9ca7eba5c4eeb..0f1473fc05492 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx @@ -9,8 +9,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../common/types'; +import { SectionError, Error } from '../../../shared_imports'; import { TIME_UNITS } from '../../../../common/constants'; -import { SectionError, SectionLoading, PolicyForm, Error } from '../../components'; +import { SectionLoading, PolicyForm } from '../../components'; import { BASE_PATH } from '../../constants'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx index 126e04bc7dc1d..08bfde833c368 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx @@ -12,7 +12,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { Repository, EmptyRepository } from '../../../../common/types'; -import { RepositoryForm, SectionError } from '../../components'; +import { SectionError } from '../../../shared_imports'; + +import { RepositoryForm } from '../../components'; import { BASE_PATH, Section } from '../../constants'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { addRepository } from '../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx index aa29b8b9f0551..95f8b9b8bde7d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx @@ -10,7 +10,8 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiCallOut, EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { Repository, EmptyRepository } from '../../../../common/types'; -import { RepositoryForm, SectionError, SectionLoading, Error } from '../../components'; +import { SectionError, Error } from '../../../shared_imports'; +import { RepositoryForm, SectionLoading } from '../../components'; import { BASE_PATH, Section } from '../../constants'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx index 252fd07a85f80..9eabed8341ee0 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx @@ -9,8 +9,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { SnapshotDetails, RestoreSettings } from '../../../../common/types'; +import { SectionError, Error } from '../../../shared_imports'; import { BASE_PATH } from '../../constants'; -import { SectionError, SectionLoading, RestoreSnapshotForm, Error } from '../../components'; +import { SectionLoading, RestoreSnapshotForm } from '../../components'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { useLoadSnapshot, executeRestore } from '../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts index 200d601fd2ce9..27a565ccb74bc 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts @@ -9,11 +9,10 @@ import { UseRequestConfig, sendRequest as _sendRequest, useRequest as _useRequest, + Error as CustomError, } from '../../../shared_imports'; -import { Error as CustomError } from '../../components/section_error'; - -import { httpService } from './index'; +import { httpService } from '.'; export const sendRequest = (config: SendRequestConfig) => { return _sendRequest(httpService.httpClient, config); diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts index 7e7ef09d0c09d..e0024ea8e0c12 100644 --- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts @@ -12,4 +12,10 @@ export { useRequest, CronEditor, DAY, + SectionError, + Error, + WithPrivileges, + useAuthorizationContext, + NotAuthorizedSection, + AuthorizationProvider, } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts index 5d334fddc144b..bda64fdb66571 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts @@ -3,12 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Privileges } from '../../../common/types'; +import { Privileges } from '../../../../../../src/plugins/es_ui_shared/public'; + import { APP_REQUIRED_CLUSTER_PRIVILEGES, APP_RESTORE_INDEX_PRIVILEGES, APP_SLM_CLUSTER_PRIVILEGES, -} from '../../../common/constants'; +} from '../../../common'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index f7962f7011f34..8e877f696a2fc 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -7,7 +7,7 @@ import { configSchema } from './config'; describe('config validation', () => { test('task manager defaults', () => { - const config: Record = {}; + const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "enabled": true, @@ -21,7 +21,7 @@ describe('config validation', () => { }); test('the ElastiSearch Tasks index cannot be used for task manager', () => { - const config: Record = { + const config: Record = { index: '.tasks', }; expect(() => { diff --git a/x-pack/plugins/task_manager/server/create_task_manager.ts b/x-pack/plugins/task_manager/server/create_task_manager.ts index 9ff97bbcc17e6..7ab6acba7976d 100644 --- a/x-pack/plugins/task_manager/server/create_task_manager.ts +++ b/x-pack/plugins/task_manager/server/create_task_manager.ts @@ -12,9 +12,10 @@ import { } from '../../../../src/core/server'; import { TaskManager } from './task_manager'; import { Logger } from './types'; +import { TaskManagerConfig } from './config'; export interface LegacyDeps { - config: any; + config: unknown; elasticsearch: Pick; savedObjectsRepository: ISavedObjectsRepository; savedObjectsSerializer: SavedObjectsSerializer; @@ -33,7 +34,7 @@ export function createTaskManager( ) { return new TaskManager({ taskManagerId: core.uuid.getInstanceUuid(), - config, + config: config as TaskManagerConfig, savedObjectsRepository, serializer: savedObjectsSerializer, callAsInternalUser, diff --git a/x-pack/plugins/task_manager/server/lib/middleware.test.ts b/x-pack/plugins/task_manager/server/lib/middleware.test.ts index 3aa39eb3db513..abf69e726262f 100644 --- a/x-pack/plugins/task_manager/server/lib/middleware.test.ts +++ b/x-pack/plugins/task_manager/server/lib/middleware.test.ts @@ -28,9 +28,9 @@ const getMockConcreteTaskInstance = () => { scheduledAt: Date; startedAt: Date | null; retryAt: Date | null; - state: any; + state: unknown; taskType: string; - params: any; + params: unknown; ownerId: string | null; } = { id: 'hy8o99o83', @@ -47,7 +47,7 @@ const getMockConcreteTaskInstance = () => { params: { abc: 'def' }, ownerId: null, }; - return concrete; + return (concrete as unknown) as ConcreteTaskInstance; }; const getMockRunContext = (runTask: ConcreteTaskInstance) => ({ taskInstance: runTask, @@ -95,7 +95,7 @@ describe('addMiddlewareToChain', () => { await middlewareChain .beforeSave({ taskInstance: getMockTaskInstance() }) - .then((saveOpts: any) => { + .then((saveOpts: unknown) => { expect(saveOpts).toMatchInlineSnapshot(` Object { "taskInstance": Object { diff --git a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts b/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts index da64befe28673..650eb36347c86 100644 --- a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts +++ b/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { RunContext } from '../task'; +import { RunContext, TaskDictionary, TaskDefinition } from '../task'; import { sanitizeTaskDefinitions } from './sanitize_task_definitions'; interface Opts { @@ -14,7 +14,7 @@ interface Opts { const getMockTaskDefinitions = (opts: Opts) => { const { numTasks } = opts; - const tasks: any = {}; + const tasks: Record = {}; for (let i = 0; i < numTasks; i++) { const type = `test_task_type_${i}`; @@ -35,7 +35,7 @@ const getMockTaskDefinitions = (opts: Opts) => { }, }; } - return tasks; + return (tasks as unknown) as TaskDictionary; }; describe('sanitizeTaskDefinitions', () => { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index e837fcd9c0dec..a70fbdb18c30b 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -35,7 +35,7 @@ export class TaskManagerPlugin this.currentConfig = {} as TaskManagerConfig; } - public setup(core: CoreSetup, plugins: any): TaskManagerSetupContract { + public setup(core: CoreSetup, plugins: unknown): TaskManagerSetupContract { const logger = this.initContext.logger.get('taskManager'); const config$ = this.initContext.config.create(); return { diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 8f7cc47f936b2..4fd4da3d83a36 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -55,6 +55,7 @@ export const IdleTaskWithExpiredRunAt: MustCondition = }; // TODO: Fix query clauses to support this +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const InactiveTasks: BoolClauseWithAnyCondition = { bool: { must_not: [ diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index 48e87582ce3fe..e806a866ff0b7 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -28,7 +28,7 @@ type Require = Omit & Required Promise; +export type ElasticJs = (action: string, args: unknown) => Promise; /** * The run context is passed into a task's run function as its sole argument. @@ -61,12 +61,12 @@ export interface RunResult { * The state which will be passed to the next run of this task (if this is a * recurring task). See the RunContext type definition for more details. */ - state: Record; + state: Record; } export interface SuccessfulRunResult { runAt?: Date; - state?: Record; + state?: Record; } export interface FailedRunResult extends SuccessfulRunResult { @@ -237,6 +237,9 @@ export interface TaskInstance { * A task-specific set of parameters, used by the task's run function to tailor * its work. This is generally user-input, such as { sms: '333-444-2222' }. */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; /** @@ -244,6 +247,9 @@ export interface TaskInstance { * run. If there was no previous run, or if the previous run did not return * any state, this will be the empy object: {} */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record; /** @@ -336,6 +342,9 @@ export interface ConcreteTaskInstance extends TaskInstance { * run. If there was no previous run, or if the previous run did not return * any state, this will be the empy object: {} */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record; /** @@ -343,3 +352,15 @@ export interface ConcreteTaskInstance extends TaskInstance { */ ownerId: string | null; } + +export type SerializedConcreteTaskInstance = Omit< + ConcreteTaskInstance, + 'state' | 'params' | 'scheduledAt' | 'startedAt' | 'retryAt' | 'runAt' +> & { + state: string; + params: string; + scheduledAt: string; + startedAt: string | null; + retryAt: string | null; + runAt: string; +}; diff --git a/x-pack/plugins/task_manager/server/task_events.ts b/x-pack/plugins/task_manager/server/task_events.ts index 063ac2499471f..b17a3636c1730 100644 --- a/x-pack/plugins/task_manager/server/task_events.ts +++ b/x-pack/plugins/task_manager/server/task_events.ts @@ -68,16 +68,18 @@ export function asTaskRunRequestEvent( } export function isTaskMarkRunningEvent( - taskEvent: TaskEvent + taskEvent: TaskEvent ): taskEvent is TaskMarkRunning { return taskEvent.type === TaskEventType.TASK_MARK_RUNNING; } -export function isTaskRunEvent(taskEvent: TaskEvent): taskEvent is TaskRun { +export function isTaskRunEvent(taskEvent: TaskEvent): taskEvent is TaskRun { return taskEvent.type === TaskEventType.TASK_RUN; } -export function isTaskClaimEvent(taskEvent: TaskEvent): taskEvent is TaskClaim { +export function isTaskClaimEvent(taskEvent: TaskEvent): taskEvent is TaskClaim { return taskEvent.type === TaskEventType.TASK_CLAIM; } -export function isTaskRunRequestEvent(taskEvent: TaskEvent): taskEvent is TaskRunRequest { +export function isTaskRunRequestEvent( + taskEvent: TaskEvent +): taskEvent is TaskRunRequest { return taskEvent.type === TaskEventType.TASK_RUN_REQUEST; } diff --git a/x-pack/plugins/task_manager/server/task_manager.test.ts b/x-pack/plugins/task_manager/server/task_manager.test.ts index 3d48ce18c9d6a..3f3b14a791f24 100644 --- a/x-pack/plugins/task_manager/server/task_manager.test.ts +++ b/x-pack/plugins/task_manager/server/task_manager.test.ts @@ -25,6 +25,7 @@ import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src import { mockLogger } from './test_utils'; import { asErr, asOk } from './lib/result_type'; import { ConcreteTaskInstance, TaskLifecycleResult, TaskStatus } from './task'; +import { Middleware } from './lib/middleware'; const savedObjectsClient = savedObjectsRepositoryMock.create(); const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); @@ -247,20 +248,20 @@ describe('TaskManager', () => { test('allows middleware registration before starting', () => { const client = new TaskManager(taskManagerOpts); - const middleware = { - beforeSave: async (saveOpts: any) => saveOpts, - beforeRun: async (runOpts: any) => runOpts, - beforeMarkRunning: async (runOpts: any) => runOpts, + const middleware: Middleware = { + beforeSave: jest.fn(async saveOpts => saveOpts), + beforeRun: jest.fn(async runOpts => runOpts), + beforeMarkRunning: jest.fn(async runOpts => runOpts), }; expect(() => client.addMiddleware(middleware)).not.toThrow(); }); test('disallows middleware registration after starting', async () => { const client = new TaskManager(taskManagerOpts); - const middleware = { - beforeSave: async (saveOpts: any) => saveOpts, - beforeRun: async (runOpts: any) => runOpts, - beforeMarkRunning: async (runOpts: any) => runOpts, + const middleware: Middleware = { + beforeSave: jest.fn(async saveOpts => saveOpts), + beforeRun: jest.fn(async runOpts => runOpts), + beforeMarkRunning: jest.fn(async runOpts => runOpts), }; client.start(); diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index a7c67d190e72e..24ceea0fe71ef 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -43,6 +43,7 @@ import { TaskLifecycle, TaskLifecycleResult, TaskStatus, + ElasticJs, } from './task'; import { createTaskPoller, PollingError, PollingErrorType } from './task_poller'; import { TaskPool } from './task_pool'; @@ -129,7 +130,7 @@ export class TaskManager { this.store = new TaskStore({ serializer: opts.serializer, savedObjectsRepository: opts.savedObjectsRepository, - callCluster: opts.callAsInternalUser, + callCluster: (opts.callAsInternalUser as unknown) as ElasticJs, index: opts.config.index, maxAttempts: opts.config.max_attempts, definitions: this.definitions, @@ -273,7 +274,7 @@ export class TaskManager { */ public async schedule( taskInstance: TaskInstanceWithDeprecatedFields, - options?: any + options?: object ): Promise { await this.waitUntilStarted(); const { taskInstance: modifiedTask } = await this.middleware.beforeSave({ @@ -308,7 +309,7 @@ export class TaskManager { */ public async ensureScheduled( taskInstance: TaskInstanceWithId, - options?: any + options?: object ): Promise { try { return await this.schedule(taskInstance, options); diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index fb87b6290a3da..dee4d80b97917 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -9,6 +9,7 @@ import { TaskPool, TaskPoolRunResult } from './task_pool'; import { mockLogger, resolvable, sleep } from './test_utils'; import { asOk } from './lib/result_type'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import moment from 'moment'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { @@ -190,14 +191,16 @@ describe('TaskPool', () => { }); test('run cancels expired tasks prior to running new tasks', async () => { + const logger = mockLogger(); const pool = new TaskPool({ maxWorkers: 2, - logger: mockLogger(), + logger, }); const expired = resolvable(); const shouldRun = sinon.spy(() => Promise.resolve()); const shouldNotRun = sinon.spy(() => Promise.resolve()); + const now = new Date(); const result = await pool.run([ { ...mockTask(), @@ -207,6 +210,16 @@ describe('TaskPool', () => { await sleep(10); return asOk({ state: {} }); }, + get expiration() { + return now; + }, + get startedAt() { + // 5 and a half minutes + return moment(now) + .subtract(5, 'm') + .subtract(30, 's') + .toDate(); + }, cancel: shouldRun, }, { @@ -231,6 +244,10 @@ describe('TaskPool', () => { expect(pool.occupiedWorkers).toEqual(2); expect(pool.availableWorkers).toEqual(0); + + expect(logger.warn).toHaveBeenCalledWith( + `Cancelling task TaskType "shooooo" as it expired at ${now.toISOString()} after running for 05m 30s (with timeout set at 5m).` + ); }); test('logs if cancellation errors', async () => { @@ -285,6 +302,20 @@ describe('TaskPool', () => { markTaskAsRunning: jest.fn(async () => true), run: mockRun(), toString: () => `TaskType "shooooo"`, + get expiration() { + return new Date(); + }, + get startedAt() { + return new Date(); + }, + get definition() { + return { + type: '', + title: '', + timeout: '5m', + createTaskRunner: jest.fn(), + }; + }, }; } }); diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 8999fb48680ce..bd0de86551aaa 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -8,7 +8,9 @@ * This module contains the logic that ensures we don't run too many * tasks at once in a given Kibana instance. */ +import moment, { Duration } from 'moment'; import { performance } from 'perf_hooks'; +import { padLeft } from 'lodash'; import { Logger } from './types'; import { TaskRunner } from './task_runner'; import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; @@ -148,7 +150,19 @@ export class TaskPool { private cancelExpiredTasks() { for (const task of this.running) { if (task.isExpired) { - this.logger.debug(`Cancelling expired task ${task.toString()}.`); + this.logger.warn( + `Cancelling task ${task.toString()} as it expired at ${task.expiration.toISOString()}${ + task.startedAt + ? ` after running for ${durationAsString( + moment.duration( + moment(new Date()) + .utc() + .diff(task.startedAt) + ) + )}` + : `` + }${task.definition.timeout ? ` (with timeout set at ${task.definition.timeout})` : ``}.` + ); this.cancelTask(task); } } @@ -169,3 +183,8 @@ function partitionListByCount(list: T[], count: number): [T[], T[]] { const listInCount = list.splice(0, count); return [listInCount, list]; } + +function durationAsString(duration: Duration): string { + const [m, s] = [duration.minutes(), duration.seconds()].map(value => padLeft(`${value}`, 2, '0')); + return `${m}m ${s}s`; +} diff --git a/x-pack/plugins/task_manager/server/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_runner.test.ts index 3f0132105347e..07247dcb1da47 100644 --- a/x-pack/plugins/task_manager/server/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_runner.test.ts @@ -9,10 +9,11 @@ import sinon from 'sinon'; import { minutesFromNow } from './lib/intervals'; import { asOk, asErr } from './lib/result_type'; import { TaskEvent, asTaskRunEvent, asTaskMarkRunningEvent } from './task_events'; -import { ConcreteTaskInstance, TaskStatus } from './task'; +import { ConcreteTaskInstance, TaskStatus, TaskDictionary, TaskDefinition } from './task'; import { TaskManagerRunner } from './task_runner'; import { mockLogger } from './test_utils'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import moment from 'moment'; let fakeTimer: sinon.SinonFakeTimers; @@ -113,6 +114,60 @@ describe('TaskManagerRunner', () => { expect(instance.runAt.getTime()).toBeLessThanOrEqual(minutesFromNow(10).getTime()); }); + test('expiration returns time after which timeout will have elapsed from start', async () => { + const now = moment(); + const { runner } = testOpts({ + instance: { + schedule: { interval: '10m' }, + status: TaskStatus.Running, + startedAt: now.toDate(), + }, + definitions: { + bar: { + timeout: `1m`, + createTaskRunner: () => ({ + async run() { + return; + }, + }), + }, + }, + }); + + await runner.run(); + + expect(runner.isExpired).toBe(false); + expect(runner.expiration).toEqual(now.add(1, 'm').toDate()); + }); + + test('runDuration returns duration which has elapsed since start', async () => { + const now = moment() + .subtract(30, 's') + .toDate(); + const { runner } = testOpts({ + instance: { + schedule: { interval: '10m' }, + status: TaskStatus.Running, + startedAt: now, + }, + definitions: { + bar: { + timeout: `1m`, + createTaskRunner: () => ({ + async run() { + return; + }, + }), + }, + }, + }); + + await runner.run(); + + expect(runner.isExpired).toBe(false); + expect(runner.startedAt).toEqual(now); + }); + test('reschedules tasks that return a runAt', async () => { const runAt = minutesFromNow(_.random(1, 10)); const { runner, store } = testOpts({ @@ -208,7 +263,7 @@ describe('TaskManagerRunner', () => { expect(logger.warn).not.toHaveBeenCalled(); }); - test('warns if cancel is called on a non-cancellable task', async () => { + test('debug logs if cancel is called on a non-cancellable task', async () => { const { runner, logger } = testOpts({ definitions: { bar: { @@ -223,10 +278,7 @@ describe('TaskManagerRunner', () => { await runner.cancel(); await promise; - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"The task bar \\"foo\\" is not cancellable."` - ); + expect(logger.debug).toHaveBeenCalledWith(`The task bar "foo" is not cancellable.`); }); test('sets startedAt, status, attempts and retryAt when claiming a task', async () => { @@ -854,8 +906,8 @@ describe('TaskManagerRunner', () => { interface TestOpts { instance?: Partial; - definitions?: any; - onTaskEvent?: (event: TaskEvent) => void; + definitions?: unknown; + onTaskEvent?: (event: TaskEvent) => void; } function testOpts(opts: TestOpts) { @@ -904,7 +956,7 @@ describe('TaskManagerRunner', () => { title: 'Bar!', createTaskRunner, }, - }), + }) as TaskDictionary, onTaskEvent: opts.onTaskEvent, }); @@ -918,7 +970,7 @@ describe('TaskManagerRunner', () => { }; } - async function testReturn(result: any, shouldBeValid: boolean) { + async function testReturn(result: unknown, shouldBeValid: boolean) { const { runner, logger } = testOpts({ definitions: { bar: { @@ -939,11 +991,11 @@ describe('TaskManagerRunner', () => { } } - function allowsReturnType(result: any) { + function allowsReturnType(result: unknown) { return testReturn(result, true); } - function disallowsReturnType(result: any) { + function disallowsReturnType(result: unknown) { return testReturn(result, false); } }); diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts index 682885aaa0b1c..7a9fa0c45e15f 100644 --- a/x-pack/plugins/task_manager/server/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_runner.ts @@ -39,6 +39,9 @@ const EMPTY_RUN_RESULT: SuccessfulRunResult = {}; export interface TaskRunner { isExpired: boolean; + expiration: Date; + startedAt: Date | null; + definition: TaskDefinition; cancel: CancelFunction; markTaskAsRunning: () => Promise; run: () => Promise>; @@ -129,11 +132,25 @@ export class TaskManagerRunner implements TaskRunner { return this.definitions[this.taskType]; } + /** + * Gets the time at which this task will expire. + */ + public get expiration() { + return intervalFromDate(this.instance.startedAt!, this.definition.timeout)!; + } + + /** + * Gets the duration of the current task run + */ + public get startedAt() { + return this.instance.startedAt; + } + /** * Gets whether or not this task has run longer than its expiration setting allows. */ public get isExpired() { - return intervalFromDate(this.instance.startedAt!, this.definition.timeout)! < new Date(); + return this.expiration < new Date(); } /** @@ -261,12 +278,12 @@ export class TaskManagerRunner implements TaskRunner { */ public async cancel() { const { task } = this; - if (task && task.cancel) { + if (task?.cancel) { this.task = undefined; return task.cancel(); } - this.logger.warn(`The task ${this} is not cancellable.`); + this.logger.debug(`The task ${this} is not cancellable.`); } private validateResult(result?: RunResult | void): Result { @@ -392,7 +409,7 @@ export class TaskManagerRunner implements TaskRunner { attempts, addDuration, }: { - error: any; + error: Error; attempts: number; addDuration?: string; }): Date | null { diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 4ecefcb7984eb..6524ea212e7c5 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -15,11 +15,17 @@ import { TaskInstance, TaskStatus, TaskLifecycleResult, + SerializedConcreteTaskInstance, + ConcreteTaskInstance, } from './task'; import { StoreOpts, OwnershipClaimingOpts, TaskStore, SearchOpts } from './task_store'; -import { savedObjectsRepositoryMock } from '../../../../src/core/server/mocks'; -import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src/core/server'; -import { SavedObjectsErrorHelpers } from '../../../../src/core/server/saved_objects/service/lib/errors'; +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { + SavedObjectsSerializer, + SavedObjectTypeRegistry, + SavedObjectAttributes, + SavedObjectsErrorHelpers, +} from 'src/core/server'; import { asTaskClaimEvent, TaskEvent } from './task_events'; import { asOk, asErr } from './lib/result_type'; @@ -47,6 +53,7 @@ const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); beforeEach(() => jest.resetAllMocks()); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date { constructor() { return mockedDate; @@ -58,9 +65,9 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); describe('TaskStore', () => { describe('schedule', () => { - async function testSchedule(task: TaskInstance) { + async function testSchedule(task: unknown) { const callCluster = jest.fn(); - savedObjectsClient.create.mockImplementation(async (type: string, attributes: any) => ({ + savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ id: 'testid', type, attributes, @@ -76,7 +83,7 @@ describe('TaskStore', () => { definitions: taskDefinitions, savedObjectsRepository: savedObjectsClient, }); - const result = await store.schedule(task); + const result = await store.schedule(task as TaskInstance); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -149,14 +156,16 @@ describe('TaskStore', () => { test('sets runAt to now if not specified', async () => { await testSchedule({ taskType: 'dernstraight', params: {}, state: {} }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes: any = savedObjectsClient.create.mock.calls[0][1]; + const attributes = savedObjectsClient.create.mock + .calls[0][1] as SerializedConcreteTaskInstance; expect(new Date(attributes.runAt as string).getTime()).toEqual(mockedDate.getTime()); }); test('ensures params and state are not null', async () => { - await testSchedule({ taskType: 'yawn' } as any); + await testSchedule({ taskType: 'yawn' }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes: any = savedObjectsClient.create.mock.calls[0][1]; + const attributes = savedObjectsClient.create.mock + .calls[0][1] as SerializedConcreteTaskInstance; expect(attributes.params).toEqual('{}'); expect(attributes.state).toEqual('{}'); }); @@ -169,8 +178,8 @@ describe('TaskStore', () => { }); describe('fetch', () => { - async function testFetch(opts?: SearchOpts, hits: any[] = []) { - const callCluster = sinon.spy(async (name: string, params?: any) => ({ hits: { hits } })); + async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { + const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ hits: { hits } })); const store = new TaskStore({ index: 'tasky', taskManagerId: '', @@ -229,11 +238,11 @@ describe('TaskStore', () => { claimingOpts, }: { opts: Partial; - hits?: any[]; + hits?: unknown[]; claimingOpts: OwnershipClaimingOpts; }) { const versionConflicts = 2; - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: hits.length + versionConflicts, @@ -266,7 +275,7 @@ describe('TaskStore', () => { } test('it returns normally with no tasks when the index does not exist.', async () => { - const callCluster = sinon.spy(async (name: string, params?: any) => ({ + const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ total: 0, updated: 0, })); @@ -745,7 +754,7 @@ if (doc['task.runAt'].size()!=0) { }; savedObjectsClient.update.mockImplementation( - async (type: string, id: string, attributes: any) => { + async (type: string, id: string, attributes: SavedObjectAttributes) => { return { id, type, @@ -1017,7 +1026,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when a task is succesfully claimed by id', async done => { const { taskManagerId, runAt, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1036,9 +1045,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent) => event.id === 'aaa')) + .pipe(filter((event: TaskEvent) => event.id === 'aaa')) .subscribe({ - next: (event: TaskEvent) => { + next: (event: TaskEvent) => { expect(event).toMatchObject( asTaskClaimEvent( 'aaa', @@ -1074,7 +1083,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when a task is succesfully by scheduling', async done => { const { taskManagerId, runAt, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1093,9 +1102,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent) => event.id === 'bbb')) + .pipe(filter((event: TaskEvent) => event.id === 'bbb')) .subscribe({ - next: (event: TaskEvent) => { + next: (event: TaskEvent) => { expect(event).toMatchObject( asTaskClaimEvent( 'bbb', @@ -1131,7 +1140,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when the store fails to claim a required task by id', async done => { const { taskManagerId, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1150,9 +1159,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent) => event.id === 'ccc')) + .pipe(filter((event: TaskEvent) => event.id === 'ccc')) .subscribe({ - next: (event: TaskEvent) => { + next: (event: TaskEvent) => { expect(event).toMatchObject( asTaskClaimEvent('ccc', asErr(new Error(`failed to claim task 'ccc'`))) ); diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 0e487386eb04d..01299615c7d49 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -9,11 +9,11 @@ */ import apm from 'elastic-apm-node'; import { Subject, Observable } from 'rxjs'; -import { omit, difference } from 'lodash'; +import { omit, difference, defaults } from 'lodash'; +import { SearchResponse, UpdateDocumentByQueryResponse } from 'elasticsearch'; import { SavedObject, - SavedObjectAttributes, SavedObjectsSerializer, SavedObjectsRawDoc, ISavedObjectsRepository, @@ -29,6 +29,7 @@ import { TaskInstance, TaskLifecycle, TaskLifecycleResult, + SerializedConcreteTaskInstance, } from './task'; import { TaskClaim, asTaskClaimEvent } from './task_events'; @@ -71,7 +72,7 @@ export interface SearchOpts { query?: object; size?: number; seq_no_primary_term?: boolean; - search_after?: any[]; + search_after?: unknown[]; } export interface UpdateByQuerySearchOpts extends SearchOpts { @@ -166,7 +167,7 @@ export class TaskStore { ); } - const savedObject = await this.savedObjectsRepository.create( + const savedObject = await this.savedObjectsRepository.create( 'task', taskInstanceToAttributes(taskInstance), { id: taskInstance.id, refresh: false } @@ -314,17 +315,21 @@ export class TaskStore { * @returns {Promise} */ public async update(doc: ConcreteTaskInstance): Promise { - const updatedSavedObject = await this.savedObjectsRepository.update( - 'task', - doc.id, - taskInstanceToAttributes(doc), - { - refresh: false, - version: doc.version, - } - ); + const attributes = taskInstanceToAttributes(doc); + const updatedSavedObject = await this.savedObjectsRepository.update< + SerializedConcreteTaskInstance + >('task', doc.id, attributes, { + refresh: false, + version: doc.version, + }); - return savedObjectToConcreteTaskInstance(updatedSavedObject); + return savedObjectToConcreteTaskInstance( + // The SavedObjects update api forces a Partial on the `attributes` on the response, + // but actually returns the whole object that is passed to it, so as we know we're + // passing in the whole object, this is safe to do. + // This is far from ideal, but unless we change the SavedObjectsClient this is the best we can do + { ...updatedSavedObject, attributes: defaults(updatedSavedObject.attributes, attributes) } + ); } /** @@ -377,12 +382,12 @@ export class TaskStore { }, }); - const rawDocs = result.hits.hits; + const rawDocs = (result as SearchResponse).hits.hits; return { docs: (rawDocs as SavedObjectsRawDoc[]) .map(doc => this.serializer.rawToSavedObject(doc)) - .map(doc => omit(doc, 'namespace') as SavedObject) + .map(doc => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), }; } @@ -404,7 +409,7 @@ export class TaskStore { }, }); - const { total, updated, version_conflicts } = result; + const { total, updated, version_conflicts } = result as UpdateDocumentByQueryResponse; return { total, updated, @@ -413,7 +418,7 @@ export class TaskStore { } } -function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes { +function taskInstanceToAttributes(doc: TaskInstance): SerializedConcreteTaskInstance { return { ...omit(doc, 'id', 'version'), params: JSON.stringify(doc.params || {}), @@ -428,8 +433,7 @@ function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes { } export function savedObjectToConcreteTaskInstance( - // TODO: define saved object type - savedObject: Omit, 'references'> + savedObject: Omit, 'references'> ): ConcreteTaskInstance { return { ...savedObject.attributes, @@ -437,8 +441,8 @@ export function savedObjectToConcreteTaskInstance( version: savedObject.version, scheduledAt: new Date(savedObject.attributes.scheduledAt), runAt: new Date(savedObject.attributes.runAt), - startedAt: savedObject.attributes.startedAt && new Date(savedObject.attributes.startedAt), - retryAt: savedObject.attributes.retryAt && new Date(savedObject.attributes.retryAt), + startedAt: savedObject.attributes.startedAt ? new Date(savedObject.attributes.startedAt) : null, + retryAt: savedObject.attributes.retryAt ? new Date(savedObject.attributes.retryAt) : null, state: parseJSONField(savedObject.attributes.state, 'state', savedObject.id), params: parseJSONField(savedObject.attributes.params, 'params', savedObject.id), }; diff --git a/x-pack/plugins/task_manager/server/test_utils/index.ts b/x-pack/plugins/task_manager/server/test_utils/index.ts index 719ccadbe33dd..3dfc53672b46f 100644 --- a/x-pack/plugins/task_manager/server/test_utils/index.ts +++ b/x-pack/plugins/task_manager/server/test_utils/index.ts @@ -33,11 +33,11 @@ interface Resolvable { */ export function resolvable(): PromiseLike & Resolvable { let resolve: () => void; - const result = new Promise(r => (resolve = r)) as any; - - result.resolve = () => nativeTimeout(resolve, 0); - - return result; + return Object.assign(new Promise(r => (resolve = r)), { + resolve() { + return nativeTimeout(resolve, 0); + }, + }); } /** diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 0db11ffa061c0..9d8106a1366d6 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// actual mocks export const expandLiteralStrings = jest.fn(); export const XJsonMode = jest.fn(); export const useRequest = jest.fn(() => ({ @@ -11,5 +12,20 @@ export const useRequest = jest.fn(() => ({ error: null, data: undefined, })); -export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; -export const SORT_DIRECTION = { ASC: 'asc' }; + +// just passing through the reimports +export { getErrorMessage } from '../../../ml/common/util/errors'; +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts new file mode 100644 index 0000000000000..0e5ecb5d3b214 --- /dev/null +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { + getPreviewRequestBody, + PivotAggsConfig, + PivotGroupByConfig, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, + SimpleQuery, +} from '../common'; + +import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; + +describe('Transform: Data Grid', () => { + test('getPivotPreviewDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const groupBy: PivotGroupByConfig = { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-group-by-field', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', + }; + const agg: PivotAggsConfig = { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-agg-field', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', + }; + const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); + const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); + + expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview +{ + "source": { + "index": [ + "the-index-pattern-title" + ] + }, + "pivot": { + "group_by": { + "the-group-by-agg-name": { + "terms": { + "field": "the-group-by-field" + } + } + }, + "aggregations": { + "the-agg-agg-name": { + "avg": { + "field": "the-agg-field" + } + } + } + } +} +`); + }); +}); + +describe('Transform: Index Preview Common', () => { + test('getIndexDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const indexPreviewDevConsoleStatement = getIndexDevConsoleStatement( + query, + 'the-index-pattern-title' + ); + + expect(indexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search +{ + "query": { + "query_string": { + "query": "*", + "default_operator": "AND" + } + } +} +`); + }); +}); diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index 0e9cceefb3156..cf9ba5d6f5853 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridStyle } from '@elastic/eui'; +import { PivotQuery } from './request'; +import { PreviewRequestBody } from './transform'; export const INIT_MAX_COLUMNS = 20; -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'highlight', - header: 'shade', +export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { + return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, +export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { + return `GET ${indexPatternTitle}/_search\n${JSON.stringify( + { + query, + }, + null, + 2 + )}\n`; }; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index daeddaa801828..009c8c7a2a9f5 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -5,7 +5,11 @@ */ export { AggName, isAggName } from './aggregations'; -export { euiDataGridStyle, euiDataGridToolbarSettings, INIT_MAX_COLUMNS } from './data_grid'; +export { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, + INIT_MAX_COLUMNS, +} from './data_grid'; export { getDefaultSelectableFields, getFlattenedFields, diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts deleted file mode 100644 index 172256ddb5cee..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts +++ /dev/null @@ -1,117 +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 { EuiDataGridSorting } from '@elastic/eui'; - -import { - getPreviewRequestBody, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, - SimpleQuery, -} from '../../common'; - -import { multiColumnSortFactory, getPivotPreviewDevConsoleStatement } from './common'; - -describe('Transform: Define Pivot Common', () => { - test('multiColumnSortFactory()', () => { - const data = [ - { s: 'a', n: 1 }, - { s: 'a', n: 2 }, - { s: 'b', n: 3 }, - { s: 'b', n: 4 }, - ]; - - const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; - const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); - data.sort(multiColumnSort1); - - expect(data).toStrictEqual([ - { s: 'b', n: 3 }, - { s: 'b', n: 4 }, - { s: 'a', n: 1 }, - { s: 'a', n: 2 }, - ]); - - const sortingColumns2: EuiDataGridSorting['columns'] = [ - { id: 's', direction: 'asc' }, - { id: 'n', direction: 'desc' }, - ]; - const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); - data.sort(multiColumnSort2); - - expect(data).toStrictEqual([ - { s: 'a', n: 2 }, - { s: 'a', n: 1 }, - { s: 'b', n: 4 }, - { s: 'b', n: 3 }, - ]); - - const sortingColumns3: EuiDataGridSorting['columns'] = [ - { id: 'n', direction: 'desc' }, - { id: 's', direction: 'desc' }, - ]; - const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); - data.sort(multiColumnSort3); - - expect(data).toStrictEqual([ - { s: 'b', n: 4 }, - { s: 'b', n: 3 }, - { s: 'a', n: 2 }, - { s: 'a', n: 1 }, - ]); - }); - - test('getPivotPreviewDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }; - const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); - const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); - - expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview -{ - "source": { - "index": [ - "the-index-pattern-title" - ] - }, - "pivot": { - "group_by": { - "the-group-by-agg-name": { - "terms": { - "field": "the-group-by-field" - } - } - }, - "aggregations": { - "the-agg-agg-name": { - "avg": { - "field": "the-agg-field" - } - } - } - } -} -`); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts deleted file mode 100644 index 498c3a3ac60af..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts +++ /dev/null @@ -1,60 +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 { EuiDataGridSorting } from '@elastic/eui'; - -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { PreviewRequestBody } from '../../common'; - -/** - * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. - * `sortFn()` is recursive to support sorting on multiple columns. - * - * @param sortingColumns - The EUI data grid sorting configuration - * @returns The sorting function which can be used with an array's sort() function. - */ -export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { - const isString = (arg: any): arg is string => { - return typeof arg === 'string'; - }; - - const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { - const sort = sortingColumns[sortingColumnIndex]; - const aValue = getNestedProperty(a, sort.id, null); - const bValue = getNestedProperty(b, sort.id, null); - - if (typeof aValue === 'number' && typeof bValue === 'number') { - if (aValue < bValue) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue > bValue) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (isString(aValue) && isString(bValue)) { - if (aValue.localeCompare(bValue) === -1) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue.localeCompare(bValue) === 1) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (sortingColumnIndex + 1 < sortingColumns.length) { - return sortFn(a, b, sortingColumnIndex + 1); - } - - return 0; - }; - - return sortFn; -}; - -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { - return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts deleted file mode 100644 index 049e73d6309fc..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { PivotPreview } from './pivot_preview'; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx deleted file mode 100644 index 5ed50eaab46ba..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx +++ /dev/null @@ -1,55 +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 React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { - getPivotQuery, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../common'; - -import { PivotPreview } from './pivot_preview'; - -jest.mock('../../../shared_imports'); -jest.mock('../../../app/app_dependencies'); - -describe('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const props = { - aggs: { 'the-agg-name': agg }, - groupBy: { 'the-group-by-name': groupBy }, - indexPatternTitle: 'the-index-pattern-title', - query: getPivotQuery('the-query'), - }; - - const { getByText } = render(); - - // Act - // Assert - expect(getByText('Transform pivot preview')).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx deleted file mode 100644 index c50df0366d698..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx +++ /dev/null @@ -1,345 +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 moment from 'moment-timezone'; -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiDataGridSorting, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiTitle, -} from '@elastic/eui'; - -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfig, - PivotGroupByConfigDict, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; - -import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common'; -import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; - -function sortColumns(groupByArr: PivotGroupByConfig[]) { - return (a: string, b: string) => { - // make sure groupBy fields are always most left columns - if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { - return a.localeCompare(b); - } - if (groupByArr.some(d => d.aggName === a)) { - return -1; - } - if (groupByArr.some(d => d.aggName === b)) { - return 1; - } - return a.localeCompare(b); - }; -} - -interface PreviewTitleProps { - previewRequest: PreviewRequestBody; -} - -const PreviewTitle: FC = ({ previewRequest }) => { - const euiCopyText = i18n.translate('xpack.transform.pivotPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', - }); - - return ( - - - - - {i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { - defaultMessage: 'Transform pivot preview', - })} - - - - - - {(copy: () => void) => ( - - )} - - - - ); -}; - -interface ErrorMessageProps { - message: string; -} - -const ErrorMessage: FC = ({ message }) => ( - - {message} - -); - -interface PivotPreviewProps { - aggs: PivotAggsConfigDict; - groupBy: PivotGroupByConfigDict; - indexPatternTitle: SearchItems['indexPattern']['title']; - query: PivotQuery; - showHeader?: boolean; -} - -const defaultPagination = { pageIndex: 0, pageSize: 5 }; - -export const PivotPreview: FC = React.memo( - ({ aggs, groupBy, indexPatternTitle, query, showHeader = true }) => { - const { - previewData: data, - previewMappings, - errorMessage, - previewRequest, - status, - } = usePivotPreviewData(indexPatternTitle, query, aggs, groupBy); - const groupByArr = dictionaryToArray(groupBy); - - // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - key => previewMappings.properties[key].type !== 'object' - ); - columnKeys.sort(sortColumns(groupByArr)); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(columnKeys.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columnKeys.join()]); - - const [pagination, setPagination] = useState(defaultPagination); - - // Reset pagination if data changes. This is to avoid ending up with an empty table - // when for example the user selected a page that is not available with the updated data. - useEffect(() => { - setPagination(defaultPagination); - }, [data.length]); - - // EuiDataGrid State - const dataGridColumns = columnKeys.map(id => { - const field = previewMappings.properties[id]; - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case ES_FIELD_TYPES.GEO_POINT: - case ES_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case ES_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - case ES_FIELD_TYPES.DATE_NANOS: - schema = 'datetime'; - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - schema = 'numeric'; - break; - // keep schema undefined for text based columns - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - break; - } - - return { id, schema }; - }); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - // Sorting config - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - if (sortingColumns.length > 0) { - data.sort(multiColumnSortFactory(sortingColumns)); - } - - const pageData = data.slice( - pagination.pageIndex * pagination.pageSize, - (pagination.pageIndex + 1) * pagination.pageSize - ); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = pageData.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - if ( - [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( - previewMappings.properties[columnId].type - ) - ) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); - - if (status === PIVOT_PREVIEW_STATUS.ERROR) { - return ( -
- - - - -
- ); - } - - if (data.length === 0) { - let noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', - { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - } - ); - - const aggsArr = dictionaryToArray(aggs); - if (aggsArr.length === 0 || groupByArr.length === 0) { - noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', - { - defaultMessage: 'Please choose at least one group-by field and aggregation.', - } - ); - } - - return ( -
- - -

{noDataMessage}

-
-
- ); - } - - if (columnKeys.length === 0) { - return null; - } - - return ( -
- {showHeader && ( - <> - -
- {status === PIVOT_PREVIEW_STATUS.LOADING && } - {status !== PIVOT_PREVIEW_STATUS.LOADING && ( - - )} -
- - )} - {dataGridColumns.length > 0 && data.length > 0 && ( - - )} -
- ); - } -); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx deleted file mode 100644 index 8d09d06b1c731..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx +++ /dev/null @@ -1,69 +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 React, { FC } from 'react'; -import ReactDOM from 'react-dom'; - -import { SimpleQuery } from '../../common'; -import { - PIVOT_PREVIEW_STATUS, - usePivotPreviewData, - UsePivotPreviewDataReturnType, -} from './use_pivot_preview_data'; - -jest.mock('../../hooks/use_api'); - -type Callback = () => void; -interface TestHookProps { - callback: Callback; -} - -const TestHook: FC = ({ callback }) => { - callback(); - return null; -}; - -const testHook = (callback: Callback) => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(, container); -}; - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -let pivotPreviewObj: UsePivotPreviewDataReturnType; - -describe('usePivotPreviewData', () => { - test('indexPattern not defined', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - test('indexPattern set triggers loading', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - // ideally this should be LOADING instead of UNUSED but jest/enzyme/hooks doesn't - // trigger that state upate yet. - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest/enzyme's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts deleted file mode 100644 index 83fa7ba189ff0..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts +++ /dev/null @@ -1,91 +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 { useEffect, useState } from 'react'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { useApi } from '../../hooks/use_api'; - -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; - -import { - getPreviewRequestBody, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfigDict, - PivotQuery, - PreviewData, - PreviewMappings, -} from '../../common'; - -export enum PIVOT_PREVIEW_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -export interface UsePivotPreviewDataReturnType { - errorMessage: string; - status: PIVOT_PREVIEW_STATUS; - previewData: PreviewData; - previewMappings: PreviewMappings; - previewRequest: PreviewRequestBody; -} - -export const usePivotPreviewData = ( - indexPatternTitle: IndexPattern['title'], - query: PivotQuery, - aggs: PivotAggsConfigDict, - groupBy: PivotGroupByConfigDict -): UsePivotPreviewDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(PIVOT_PREVIEW_STATUS.UNUSED); - const [previewData, setPreviewData] = useState([]); - const [previewMappings, setPreviewMappings] = useState({ properties: {} }); - const api = useApi(); - - const aggsArr = dictionaryToArray(aggs); - const groupByArr = dictionaryToArray(groupBy); - - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - - const getPreviewData = async () => { - if (aggsArr.length === 0 || groupByArr.length === 0) { - setPreviewData([]); - return; - } - - setErrorMessage(''); - setStatus(PIVOT_PREVIEW_STATUS.LOADING); - - try { - const resp = await api.getTransformsPreview(previewRequest); - setPreviewData(resp.preview); - setPreviewMappings(resp.generated_dest_index.mappings); - setStatus(PIVOT_PREVIEW_STATUS.LOADED); - } catch (e) { - setErrorMessage(JSON.stringify(e, null, 2)); - setPreviewData([]); - setPreviewMappings({ properties: {} }); - setStatus(PIVOT_PREVIEW_STATUS.ERROR); - } - }; - - useEffect(() => { - getPreviewData(); - // custom comparison - /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - indexPatternTitle, - JSON.stringify(aggsArr), - JSON.stringify(groupByArr), - JSON.stringify(query), - /* eslint-enable react-hooks/exhaustive-deps */ - ]); - - return { errorMessage, status, previewData, previewMappings, previewRequest }; -}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx new file mode 100644 index 0000000000000..4ca536e3c115d --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { render, wait } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import '@testing-library/jest-dom/extend-expect'; + +import { CoreSetup } from 'src/core/public'; + +import { DataGrid, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; + +import { SimpleQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useIndexData } from './use_index_data'; + +jest.mock('../../shared_imports'); +jest.mock('../app_dependencies'); +jest.mock('./use_api'); + +const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, +}; + +describe('Transform: useIndexData()', () => { + test('indexPattern set triggers loading', async done => { + const { result, waitForNextUpdate } = renderHook(() => + useIndexData( + ({ + id: 'the-id', + title: 'the-title', + fields: [], + } as unknown) as SearchItems['indexPattern'], + query + ) + ); + const IndexObj: UseIndexDataReturnType = result.current; + + await waitForNextUpdate(); + + expect(IndexObj.errorMessage).toBe(''); + expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); + expect(IndexObj.tableItems).toEqual([]); + done(); + }); +}); + +describe('Transform: with useIndexData()', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { + // Arrange + const indexPattern = { + title: 'the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const Wrapper = () => { + const props = { + ...useIndexData(indexPattern, { match_all: {} }), + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], + }; + + return ; + }; + const { getByText } = render(); + + // Act + // Assert + expect(getByText('the-index-preview-title')).toBeInTheDocument(); + await wait(); + done(); + }); +}); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts new file mode 100644 index 0000000000000..ec5a4d244c152 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -0,0 +1,111 @@ +/* + * 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 { useEffect } from 'react'; + +import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + getErrorMessage, + useDataGrid, + useRenderCellValue, + EsSorting, + SearchResponse7, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +type IndexSearchResponse = SearchResponse7; + +export const useIndexData = ( + indexPattern: SearchItems['indexPattern'], + query: PivotQuery +): UseIndexDataReturnType => { + const api = useApi(); + + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + + // EuiDataGrid State + const columns = [ + ...indexPatternFields.map(id => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { id, schema }; + }), + ]; + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + const getIndexData = async function() { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const esSearchRequest = { + index: indexPattern.title, + body: { + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + query: isDefaultQuery(query) ? matchAllQuery : query, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map(d => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts new file mode 100644 index 0000000000000..ff7ca5d42b5f7 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -0,0 +1,240 @@ +/* + * 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 moment from 'moment-timezone'; +import { useEffect, useMemo, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; + +import { dictionaryToArray } from '../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../common/utils/object_utils'; + +import { + getErrorMessage, + multiColumnSortFactory, + useDataGrid, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { + getPreviewRequestBody, + PivotAggsConfigDict, + PivotGroupByConfigDict, + PivotGroupByConfig, + PivotQuery, + PreviewMappings, +} from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +function sortColumns(groupByArr: PivotGroupByConfig[]) { + return (a: string, b: string) => { + // make sure groupBy fields are always most left columns + if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { + return a.localeCompare(b); + } + if (groupByArr.some(d => d.aggName === a)) { + return -1; + } + if (groupByArr.some(d => d.aggName === b)) { + return 1; + } + return a.localeCompare(b); + }; +} + +export const usePivotData = ( + indexPatternTitle: SearchItems['indexPattern']['title'], + query: PivotQuery, + aggs: PivotAggsConfigDict, + groupBy: PivotGroupByConfigDict +): UseIndexDataReturnType => { + const [previewMappings, setPreviewMappings] = useState({ properties: {} }); + const api = useApi(); + + const aggsArr = dictionaryToArray(aggs); + const groupByArr = dictionaryToArray(groupBy); + + // Filters mapping properties of type `object`, which get returned for nested field parents. + const columnKeys = Object.keys(previewMappings.properties).filter( + key => previewMappings.properties[key].type !== 'object' + ); + columnKeys.sort(sortColumns(groupByArr)); + + // EuiDataGrid State + const columns = columnKeys.map(id => { + const field = previewMappings.properties[id]; + + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case ES_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: + schema = 'datetime'; + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + schema = 'numeric'; + break; + // keep schema undefined for text based columns + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + break; + } + + return { id, schema }; + }); + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setNoDataMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); + + const getPreviewData = async () => { + if (aggsArr.length === 0 || groupByArr.length === 0) { + setTableItems([]); + setRowCount(0); + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', { + defaultMessage: 'Please choose at least one group-by field and aggregation.', + }) + ); + return; + } + + setErrorMessage(''); + setNoDataMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resp = await api.getTransformsPreview(previewRequest); + setTableItems(resp.preview); + setRowCount(resp.preview.length); + setPreviewMappings(resp.generated_dest_index.mappings); + setStatus(INDEX_STATUS.LOADED); + + if (resp.preview.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); + } + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setTableItems([]); + setRowCount(0); + setPreviewMappings({ properties: {} }); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + useEffect(() => { + getPreviewData(); + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + indexPatternTitle, + JSON.stringify(aggsArr), + JSON.stringify(groupByArr), + JSON.stringify(query), + /* eslint-enable react-hooks/exhaustive-deps */ + ]); + + if (sortingColumns.length > 0) { + tableItems.sort(multiColumnSortFactory(sortingColumns)); + } + + const pageData = tableItems.slice( + pagination.pageIndex * pagination.pageSize, + (pagination.pageIndex + 1) * pagination.pageSize + ); + + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = pageData.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if ( + [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( + previewMappings.properties[columnId].type + ) + ) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { + return cellValue ? 'true' : 'false'; + } + + return cellValue; + }; + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap deleted file mode 100644 index b668c7d8e4a69..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Test against strings, objects and arrays. 1`] = ` - - - - name - : - - - - the-name -    - - - - - nested.inner1 - : - - - - the-inner-1 -    - - - - - nested.inner2 - : - - - - the-inner-2 -    - - - - - arrayString - : - - - - ["the-array-string-1","the-array-string-2"] -    - - - - - arrayObject - : - - - - [{"object1":"the-object-1"},{"object2":"the-objects-2"}] -    - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts deleted file mode 100644 index d3bf81bba2e56..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.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 { SimpleQuery } from '../../../../common'; - -import { getSourceIndexDevConsoleStatement } from './common'; - -describe('Transform: Source Index Preview Common', () => { - test('getSourceIndexDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const sourceIndexPreviewDevConsoleStatement = getSourceIndexDevConsoleStatement( - query, - 'the-index-pattern-title' - ); - - expect(sourceIndexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search -{ - "query": { - "query_string": { - "query": "*", - "default_operator": "AND" - } - } -} -`); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts deleted file mode 100644 index c34675463bf8b..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts +++ /dev/null @@ -1,17 +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 { PivotQuery } from '../../../../common'; - -export const getSourceIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { - return `GET ${indexPatternTitle}/_search\n${JSON.stringify( - { - query, - }, - null, - 2 - )}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx deleted file mode 100644 index ddd1a1482fd35..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx +++ /dev/null @@ -1,46 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; -import { getFlattenedFields } from '../../../../common'; - -import { ExpandedRow } from './expanded_row'; - -describe('Transform: ', () => { - test('Test against strings, objects and arrays.', () => { - const source = { - name: 'the-name', - nested: { - inner1: 'the-inner-1', - inner2: 'the-inner-2', - }, - arrayString: ['the-array-string-1', 'the-array-string-2'], - arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], - } as Record; - - const flattenedSource = getFlattenedFields(source).reduce((p, c) => { - p[c] = getNestedProperty(source, c); - if (p[c] === undefined) { - p[c] = source[`"${c}"`]; - } - return p; - }, {} as Record); - - const props = { - item: { - _id: 'the-id', - _source: flattenedSource, - }, - }; - - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx deleted file mode 100644 index 9b83a3e5da8a8..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx +++ /dev/null @@ -1,22 +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 React from 'react'; - -import { EuiBadge, EuiText } from '@elastic/eui'; - -import { EsDoc } from '../../../../common'; - -export const ExpandedRow: React.FC<{ item: EsDoc }> = ({ item }) => ( - - {Object.entries(item._source).map(([k, value]) => ( - - {k}: - {typeof value === 'string' ? value : JSON.stringify(value)}   - - ))} - -); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts deleted file mode 100644 index a13e678813a00..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { SourceIndexPreview } from './source_index_preview'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx deleted file mode 100644 index 32f6ff9490a0f..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx +++ /dev/null @@ -1,38 +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 React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { getPivotQuery } from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; - -import { SourceIndexPreview } from './source_index_preview'; - -jest.mock('../../../../../shared_imports'); -jest.mock('../../../../../app/app_dependencies'); - -describe('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const props = { - indexPattern: { - title: 'the-index-pattern-title', - fields: [] as any[], - } as SearchItems['indexPattern'], - query: getPivotQuery('the-query'), - }; - const { getByText } = render(); - - // Act - // Assert - expect(getByText(`Source index ${props.indexPattern.title}`)).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx deleted file mode 100644 index bcdeb7ddb0d36..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ /dev/null @@ -1,293 +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 moment from 'moment-timezone'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; -import { useToastNotifications } from '../../../../app_dependencies'; - -import { getSourceIndexDevConsoleStatement } from './common'; -import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data'; - -interface SourceIndexPreviewTitle { - indexPatternTitle: string; -} -const SourceIndexPreviewTitle: React.FC = ({ indexPatternTitle }) => ( - - - {i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternTitle', { - defaultMessage: 'Source index {indexPatternTitle}', - values: { indexPatternTitle }, - })} - - -); - -interface Props { - indexPattern: SearchItems['indexPattern']; - query: PivotQuery; -} - -export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, query }) => { - const toastNotifications = useToastNotifications(); - const allFields = indexPattern.fields.map(f => f.name); - const indexPatternFields: string[] = allFields.filter(f => { - if (indexPattern.metaFields.includes(f)) { - return false; - } - - const fieldParts = f.split('.'); - const lastPart = fieldParts.pop(); - if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { - return false; - } - - return true; - }); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(indexPatternFields.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPatternFields.join()]); - - const { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems: data, - } = useSourceIndexData(indexPattern, query); - - // EuiDataGrid State - const dataGridColumns = [ - ...indexPatternFields.map(id => { - const field = indexPattern.fields.getByName(id); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case KBN_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case KBN_FIELD_TYPES.DATE: - schema = 'datetime'; - break; - case KBN_FIELD_TYPES.GEO_POINT: - case KBN_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case KBN_FIELD_TYPES.NUMBER: - schema = 'numeric'; - break; - } - - return { id, schema }; - }), - ]; - - const onSort = useCallback( - (sc: Array<{ id: string; direction: 'asc' | 'desc' }>) => { - // Check if an unsupported column type for sorting was selected. - const invalidSortingColumnns = sc.reduce((arr, current) => { - const columnType = dataGridColumns.find(dgc => dgc.id === current.id); - if (columnType?.schema === 'json') { - arr.push(current.id); - } - return arr; - }, []); - if (invalidSortingColumnns.length === 0) { - setSortingColumns(sc); - } else { - invalidSortingColumnns.forEach(columnId => { - toastNotifications.addDanger( - i18n.translate('xpack.transform.sourceIndexPreview.invalidSortingColumnError', { - defaultMessage: `The column '{columnId}' cannot be used for sorting.`, - values: { columnId }, - }) - ); - }); - } - }, - [dataGridColumns, setSortingColumns, toastNotifications] - ); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = data.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(data[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - const field = indexPattern.fields.getByName(columnId); - if (field?.type === KBN_FIELD_TYPES.DATE) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (field?.type === KBN_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [data, indexPattern.fields, pagination.pageIndex, pagination.pageSize]); - - if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) { - return ( -
- - -

- {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', { - defaultMessage: - 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', - })} -

-
-
- ); - } - - const euiCopyText = i18n.translate('xpack.transform.sourceIndexPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', - }); - - return ( -
- - - - - - - {(copy: () => void) => ( - - )} - - - -
- {status === SOURCE_INDEX_STATUS.LOADING && } - {status !== SOURCE_INDEX_STATUS.LOADING && ( - - )} -
- {status === SOURCE_INDEX_STATUS.ERROR && ( -
- - - {errorMessage} - - - -
- )} - -
- ); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx deleted file mode 100644 index 5a1d8a8db5b42..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ /dev/null @@ -1,43 +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 { renderHook } from '@testing-library/react-hooks'; -import '@testing-library/jest-dom/extend-expect'; - -import { SimpleQuery } from '../../../../common'; -import { - SOURCE_INDEX_STATUS, - useSourceIndexData, - UseSourceIndexDataReturnType, -} from './use_source_index_data'; - -jest.mock('../../../../hooks/use_api'); - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -describe('useSourceIndexData', () => { - test('indexPattern set triggers loading', async done => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query) - ); - const sourceIndexObj: UseSourceIndexDataReturnType = result.current; - - await waitForNextUpdate(); - - expect(sourceIndexObj.errorMessage).toBe(''); - expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.LOADING); - expect(sourceIndexObj.tableItems).toEqual([]); - done(); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts deleted file mode 100644 index 5301a3c168a51..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ /dev/null @@ -1,143 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; - -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IIndexPattern } from 'src/plugins/data/public'; - -import { Dictionary } from '../../../../../../common/types/common'; - -import { isDefaultQuery, matchAllQuery, EsDocSource, PivotQuery } from '../../../../common'; -import { useApi } from '../../../../hooks/use_api'; - -export enum SOURCE_INDEX_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -interface ErrorResponse { - request: Dictionary; - response: Dictionary; - body: { - statusCode: number; - error: string; - message: string; - }; - name: string; - req: Dictionary; - res: Dictionary; -} - -const isErrorResponse = (arg: any): arg is ErrorResponse => { - return arg?.body?.error !== undefined && arg?.body?.message !== undefined; -}; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -type SourceIndexSearchResponse = SearchResponse7; - -type SourceIndexPagination = Pick; -const defaultPagination: SourceIndexPagination = { pageIndex: 0, pageSize: 5 }; - -export interface UseSourceIndexDataReturnType { - errorMessage: string; - pagination: SourceIndexPagination; - setPagination: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: SOURCE_INDEX_STATUS; - tableItems: EsDocSource[]; -} - -export const useSourceIndexData = ( - indexPattern: IIndexPattern, - query: PivotQuery -): UseSourceIndexDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED); - const [pagination, setPagination] = useState(defaultPagination); - const [sortingColumns, setSortingColumns] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState([]); - const api = useApi(); - - useEffect(() => { - setPagination(defaultPagination); - }, [query]); - - const getSourceIndexData = async function() { - setErrorMessage(''); - setStatus(SOURCE_INDEX_STATUS.LOADING); - - const sort: EsSorting = sortingColumns.reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const esSearchRequest = { - index: indexPattern.title, - body: { - // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - query: isDefaultQuery(query) ? matchAllQuery : query, - from: pagination.pageIndex * pagination.pageSize, - size: pagination.pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }; - - try { - const resp: SourceIndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map(d => d._source); - - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(SOURCE_INDEX_STATUS.LOADED); - } catch (e) { - if (isErrorResponse(e)) { - setErrorMessage(`${e.body.error}: ${e.body.message}`); - } else { - setErrorMessage(JSON.stringify(e, null, 2)); - } - setStatus(SOURCE_INDEX_STATUS.ERROR); - } - }; - - useEffect(() => { - getSourceIndexData(); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); - return { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems, - }; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 320e405b5d437..0e6e2c1a38d0e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,19 +35,19 @@ import { import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { DataGrid } from '../../../../../shared_imports'; + +import { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, +} from '../../../../common/data_grid'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; +import { useIndexData } from '../../../../hooks/use_index_data'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { useToastNotifications } from '../../../../app_dependencies'; -import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; -import { DropDown } from '../aggregation_dropdown'; -import { AggListForm } from '../aggregation_list'; -import { GroupByListForm } from '../group_by_list'; -import { SourceIndexPreview } from '../source_index_preview'; -import { SwitchModal } from './switch_modal'; - import { getPivotQuery, getPreviewRequestBody, @@ -61,11 +61,17 @@ import { PivotGroupByConfig, PivotGroupByConfigDict, PivotSupportedGroupByAggs, + TransformPivotConfig, PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; +import { DropDown } from '../aggregation_dropdown'; +import { AggListForm } from '../aggregation_list'; +import { GroupByListForm } from '../group_by_list'; + import { getPivotDropdownOptions } from './common'; +import { SwitchModal } from './switch_modal'; export interface StepDefineExposedState { aggList: PivotAggsConfigDict; @@ -296,7 +302,6 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, return; } } catch (e) { - console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console setErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -593,6 +598,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, /* eslint-enable react-hooks/exhaustive-deps */ ]); + const indexPreviewProps = useIndexData(indexPattern, pivotQuery); + const pivotPreviewProps = usePivotData(indexPattern.title, pivotQuery, aggList, groupByList); + // TODO This should use the actual value of `indices.query.bool.max_clause_count` const maxIndexFields = 1024; const numIndexFields = indexPattern.fields.length; @@ -973,13 +981,37 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, - + - diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index f31514e67003b..b9021f4ee5b11 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -17,8 +17,19 @@ import { EuiText, } from '@elastic/eui'; -import { getPivotQuery, isDefaultQuery, isMatchAllQuery } from '../../../../common'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { dictionaryToArray } from '../../../../../../common/types/common'; + +import { DataGrid } from '../../../../../shared_imports'; + +import { useToastNotifications } from '../../../../app_dependencies'; +import { + getPivotQuery, + getPivotPreviewDevConsoleStatement, + getPreviewRequestBody, + isDefaultQuery, + isMatchAllQuery, +} from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -35,8 +46,25 @@ export const StepDefineSummary: FC = ({ formState: { searchString, searchQuery, groupByList, aggList }, searchItems, }) => { + const toastNotifications = useToastNotifications(); + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); + const previewRequest = getPreviewRequestBody( + searchItems.indexPattern.title, + pivotQuery, + pivotGroupByArr, + pivotAggsArr + ); + + const pivotPreviewProps = usePivotData( + searchItems.indexPattern.title, + pivotQuery, + aggList, + groupByList + ); + return ( @@ -117,11 +145,20 @@ export const StepDefineSummary: FC = ({ - diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 9a39616fb0989..23f482b5bc76a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -240,7 +240,6 @@ export const StepDetailsForm: FC = React.memo( ]} > setTransformId(e.target.value)} aria-label={i18n.translate( @@ -257,15 +256,12 @@ export const StepDetailsForm: FC = React.memo( label={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionLabel', { defaultMessage: 'Transform description', })} - helpText={i18n.translate( - 'xpack.transform.stepDetailsForm.transformDescriptionHelpText', - { - defaultMessage: 'Optional descriptive text.', - } - )} > setTransformDescription(e.target.value)} aria-label={i18n.translate( @@ -310,7 +306,6 @@ export const StepDetailsForm: FC = React.memo( } > setDestinationIndex(e.target.value)} aria-label={i18n.translate( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index eaaedc2eb77ce..e183712b390cf 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,37 +6,39 @@ import React, { FC } from 'react'; -import { SearchItems } from '../../../../hooks/use_search_items'; +import { DataGrid } from '../../../../../shared_imports'; +import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, TransformPivotConfig } from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { SearchItems } from '../../../../hooks/use_search_items'; import { applyTransformConfigToDefineState, getDefaultStepDefineState, } from '../../../create_transform/components/step_define/'; -import { PivotPreview } from '../../../../components/pivot_preview'; -interface Props { +interface ExpandedRowPreviewPaneProps { transformConfig: TransformPivotConfig; } -export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { - const previewConfig = applyTransformConfigToDefineState( +export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { + const toastNotifications = useToastNotifications(); + const { aggList, groupByList, searchQuery } = applyTransformConfigToDefineState( getDefaultStepDefineState({} as SearchItems), transformConfig ); - + const pivotQuery = getPivotQuery(searchQuery); const indexPatternTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; + const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); return ( - ); }; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 494b6db6aafe0..bcd8e53e3d191 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -17,3 +17,18 @@ export { } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { getErrorMessage } from '../../ml/common/util/errors'; + +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 209a3f626272f..5b55c7a15f1b3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -548,6 +548,8 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", "dashboard.factory.displayName": "ダッシュボード", "dashboard.panel.removePanel.replacePanel": "パネルの交換", + "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", + "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", "data.common.kql.errors.fieldNameText": "フィールド名", @@ -669,6 +671,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "このリクエストは Elasticsearch にクエリし、ビジュアライゼーション用のデータを取得します。", "data.functions.esaggs.inspector.dataRequest.title": "データ", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", @@ -2002,8 +2005,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "にアクセスして有効な別のドキュメントを選択してください。", "kbn.context.unableToLoadAnchorDocumentDescription": "別のドキュメントが読み込めません", "kbn.context.unableToLoadDocumentDescription": "ドキュメントが読み込めません", - "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", - "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "kbn.dashboardTitle": "ダッシュボード", "kbn.devToolsTitle": "開発ツール", "kbn.discover.backToTopLinkText": "最上部へ戻る。", @@ -2186,10 +2187,8 @@ "kbn.management.createIndexPatternHeader": "{indexPatternName} の作成", "kbn.management.createIndexPatternLabel": "Kibana は、可視化などを目的に Elasticsearch インデックスからデータを取得するために、インデックスパターンを使用します。", "kbn.management.editIndexPattern.deleteButton": "削除", - "kbn.management.editIndexPattern.deleteFieldButton": "削除", "kbn.management.editIndexPattern.deleteHeader": "インデックスパターンを削除しますか?", "kbn.management.editIndexPattern.detailsAria": "インデックスパターンの詳細", - "kbn.management.editIndexPattern.editFieldButton": "編集", "kbn.management.editIndexPattern.fields.allLangsDropDown": "すべての言語", "kbn.management.editIndexPattern.fields.allTypesDropDown": "すべてのフィールドタイプ", "kbn.management.editIndexPattern.fields.filterAria": "フィルター", @@ -2215,7 +2214,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "タイプ", "kbn.management.editIndexPattern.mappingConflictHeader": "マッピングの矛盾", "kbn.management.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {フィールドが} other {# フィールドが}}このパターンと一致するインデックスの間で異なるタイプ (文字列、整数など) に定義されています。これらの矛盾したフィールドは Kibana の一部で使用できますが、Kibana がタイプを把握しなければならない機能には使用できません。この問題を修正するにはデータのレンダリングが必要です。", - "kbn.management.editIndexPattern.notDateErrorMessage": "このフィールドは日付ではなく {fieldType} です。", "kbn.management.editIndexPattern.refreshAria": "フィールドリストを再度読み込みます", "kbn.management.editIndexPattern.refreshButton": "更新", "kbn.management.editIndexPattern.refreshHeader": "フィールドリストを更新しますか?", @@ -2295,6 +2293,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可視化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", @@ -2416,8 +2416,6 @@ "savedObjectsManagement.breadcrumb.index": "保存されたオブジェクト", "savedObjectsManagement.field.offLabel": "オフ", "savedObjectsManagement.field.onLabel": "オン", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可視化", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "kibana_legacy.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", @@ -2431,7 +2429,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "すべて", "kibana_utils.defaultFeedbackMessage": "フィードバックがありますか?{link} で問題を報告してください。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", - "kibana_utils.indexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", @@ -5021,8 +5018,6 @@ "xpack.canvas.elements.bubbleChartHelpText": "カスタマイズ可能なバブルチャートです", "xpack.canvas.elements.debugDisplayName": "デバッグ", "xpack.canvas.elements.debugHelpText": "エレメントの構成をダンプします", - "xpack.canvas.elements.donutChartDisplayName": "ドーナッツチャート", - "xpack.canvas.elements.donutChartHelpText": "カスタマイズ可能なドーナッツチャートです", "xpack.canvas.elements.dropdownFilterDisplayName": "ドロップダウンフィルター", "xpack.canvas.elements.dropdownFilterHelpText": "「exactly」フィルターの値を選択できるドロップダウンです", "xpack.canvas.elements.horizontalBarChartDisplayName": "水平棒グラフ", @@ -5057,8 +5052,6 @@ "xpack.canvas.elements.shapeHelpText": "カスタマイズ可能な図形です", "xpack.canvas.elements.tableDisplayName": "データテーブル", "xpack.canvas.elements.tableHelpText": "データをチューブ形式で表示する、スクロール可能なグリッドです", - "xpack.canvas.elements.tiltedPieDisplayName": "傾き円グラフ", - "xpack.canvas.elements.tiltedPieHelpText": "カスタマイズ可能な傾き円グラフです", "xpack.canvas.elements.timeFilterDisplayName": "時間フィルター", "xpack.canvas.elements.timeFilterHelpText": "期間を設定します", "xpack.canvas.elements.verticalBarChartDisplayName": "垂直棒グラフ", @@ -5069,16 +5062,16 @@ "xpack.canvas.elements.verticalProgressPillHelpText": "進捗状況を垂直のピルで表示します", "xpack.canvas.elementSettings.dataTabLabel": "データ", "xpack.canvas.elementSettings.displayTabLabel": "表示", - "xpack.canvas.elementTypes.addNewElementDescription": "ワークパッドのエレメントをグループ化して保存し、新規エレメントを作成します", - "xpack.canvas.elementTypes.addNewElementTitle": "新規エレメントの作成", - "xpack.canvas.elementTypes.cancelButtonLabel": "キャンセル", - "xpack.canvas.elementTypes.deleteButtonLabel": "削除", - "xpack.canvas.elementTypes.deleteElementDescription": "このエレメントを削除してよろしいですか?", - "xpack.canvas.elementTypes.deleteElementTitle": "エレメント「{elementName}」を削除しますか?", - "xpack.canvas.elementTypes.editElementTitle": "エレメントを編集", - "xpack.canvas.elementTypes.elementsTitle": "エレメント", - "xpack.canvas.elementTypes.findElementPlaceholder": "エレメントを検索", - "xpack.canvas.elementTypes.myElementsTitle": "マイエレメント", + "xpack.canvas.savedElementsModal.addNewElementDescription": "ワークパッドのエレメントをグループ化して保存し、新規エレメントを作成します", + "xpack.canvas.savedElementsModal.addNewElementTitle": "新規エレメントの作成", + "xpack.canvas.savedElementsModal.cancelButtonLabel": "キャンセル", + "xpack.canvas.savedElementsModal.deleteButtonLabel": "削除", + "xpack.canvas.savedElementsModal.deleteElementDescription": "このエレメントを削除してよろしいですか?", + "xpack.canvas.savedElementsModal.deleteElementTitle": "エレメント「{elementName}」を削除しますか?", + "xpack.canvas.savedElementsModal.editElementTitle": "エレメントを編集", + "xpack.canvas.savedElementsModal.elementsTitle": "エレメント", + "xpack.canvas.savedElementsModal.findElementPlaceholder": "エレメントを検索", + "xpack.canvas.savedElementsModal.myElementsTitle": "マイエレメント", "xpack.canvas.embedObject.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", "xpack.canvas.embedObject.titleText": "オブジェクトの埋め込み", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "無効な引数インデックス: {index}", @@ -5591,13 +5584,8 @@ "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "一番上", "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "グループ解除", "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "縦", - "xpack.canvas.tags.chartTag": "チャート", - "xpack.canvas.tags.filterTag": "フィルター", - "xpack.canvas.tags.graphicTag": "グラフィック", "xpack.canvas.tags.presentationTag": "プレゼンテーション", - "xpack.canvas.tags.proportionTag": "比率", "xpack.canvas.tags.reportTag": "レポート", - "xpack.canvas.tags.textTag": "テキスト", "xpack.canvas.templates.darkHelp": "ダークカラーテーマのプレゼンテーションデッキです", "xpack.canvas.templates.darkName": "ダーク", "xpack.canvas.templates.lightHelp": "ライトカラーテーマのプレゼンテーションデッキです", @@ -5897,7 +5885,6 @@ "xpack.canvas.workpadHeader.cycleIntervalHoursText": "{hours} {hours, plural, one {時間} other {時間}}ごと", "xpack.canvas.workpadHeader.cycleIntervalMinutesText": "{minutes} {minutes, plural, one {分} other {分}}ごと", "xpack.canvas.workpadHeader.cycleIntervalSecondsText": "{seconds} {seconds, plural, one {秒} other {秒}}ごと", - "xpack.canvas.workpadHeader.embedObjectButtonLabel": "オブジェクトを埋め込む", "xpack.canvas.workpadHeader.fullscreenButtonAriaLabel": "全画面表示", "xpack.canvas.workpadHeader.fullscreenTooltip": "全画面モードを開始します", "xpack.canvas.workpadHeader.hideEditControlTooltip": "編集コントロールを非表示にします", @@ -5907,7 +5894,6 @@ "xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel": "自動更新間隔を変更します", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText": "手動で", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle": "エレメントを更新", - "xpack.canvas.workpadHeaderControlSettings.settingsTooltip": "設定をコントロールします", "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "設定", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "{secondsExample}、{minutesExample}、{hoursExample} のような短い表記を使用します", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "カスタム間隔を設定", @@ -5916,32 +5902,32 @@ "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "スライドを自動的にサイクル", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "エレメントを更新", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "データを更新", - "xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage": "{PDF} 生成 {URL} がクリップボードにコピーされました。", - "xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage": "レポート構成がクリップボードにコピーされました", - "xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage": "「{workpadName}」の {PDF} の作成に失敗しました", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage": "{PDF} をエクスポート中です。管理で進捗を確認できます。", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle": "ワークパッド「{workpadName}」の {PDF} エクスポート", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel": "この {URL} を使用してスクリプトから、または Watcher で {PDF} を生成することもできます。{URL} をクリップボードにコピーするにはエンターキーを押してください。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel": "{POST} {URL} をコピー", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription": "{POST} {URL} をコピーして {KIBANA} 外または ウォッチャー から生成することもできます。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel": "{PDF} を生成", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription": "ワークパッドのサイズによって、{PDF} の生成には数分かかる場合があります。", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle": "{JSON} をダウンロード", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle": "{PDF} レポート", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle": "「{workpadName}」の {ZIP} ファイルの作成に失敗しました。ワークパッドが大きすぎる可能性があります。ファイルを別々にダウンロードする必要があります。", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle": "Web サイトで共有", - "xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage": "このワークパッドを共有", - "xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage": "未知のエクスポートタイプ: {type}", - "xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning": "このワークパッドには {CANVAS} シェアラブルワークパッドランタイムがサポートしていないレンダリング関数が含まれています。これらのエレメントはレンダリングされません:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel": "ズームコントロール", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip": "ズームコントロール", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText": "ウィンドウに合わせる", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomInText": "ズームイン", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText": "ズームアウト", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle": "ズーム:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue": "リセット", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText": "{scalePercentage}%", + "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} がクリップボードにコピーされました。", + "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "レポート構成がクリップボードにコピーされました", + "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", + "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "「{workpadName}」の {PDF} の作成に失敗しました", + "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "{PDF} をエクスポート中です。管理で進捗を確認できます。", + "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "ワークパッド「{workpadName}」の {PDF} エクスポート", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "この {URL} を使用してスクリプトから、または Watcher で {PDF} を生成することもできます。{URL} をクリップボードにコピーするにはエンターキーを押してください。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "{POST} {URL} をコピー", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "{POST} {URL} をコピーして {KIBANA} 外または ウォッチャー から生成することもできます。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "{PDF} を生成", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "ワークパッドのサイズによって、{PDF} の生成には数分かかる場合があります。", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "{JSON} をダウンロード", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} レポート", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle": "「{workpadName}」の {ZIP} ファイルの作成に失敗しました。ワークパッドが大きすぎる可能性があります。ファイルを別々にダウンロードする必要があります。", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle": "Web サイトで共有", + "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "このワークパッドを共有", + "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "未知のエクスポートタイプ: {type}", + "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "このワークパッドには {CANVAS} シェアラブルワークパッドランタイムがサポートしていないレンダリング関数が含まれています。これらのエレメントはレンダリングされません:", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "ズームコントロール", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "ズームコントロール", + "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "ウィンドウに合わせる", + "xpack.canvas.workpadHeaderViewMenu.zoomInText": "ズームイン", + "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "ズームアウト", + "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム:", + "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット", + "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} のコピー", "xpack.canvas.workpadLoader.cloneTooltip": "ワークパッドのクローンを作成します", "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "ワークパッドを作成中...", @@ -6098,9 +6084,6 @@ "xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "一時停止中", "xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "ステータス", "xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "フォロワーインデックスの接尾辞", - "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません", - "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", - "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "キャンセル", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "削除", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "{count} 個の自動フォローパターンを削除しますか?", @@ -8179,7 +8162,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "マップビュー", "xpack.infra.viewSwitcher.tableViewLabel": "表ビュー", "xpack.infra.waffle.accountAllTitle": "すべて", - "xpack.infra.waffle.accountLabel": "アカウント: {selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "{field} の平均", "xpack.infra.waffle.aggregationNames.max": "{field} の最大値", "xpack.infra.waffle.aggregationNames.min": "{field} の最小値", @@ -8215,11 +8197,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "カスタムメトリックの変更を保存", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "すべて", - "xpack.infra.waffle.groupByButtonLabel": "グループ分けの条件: ", - "xpack.infra.waffle.inventoryButtonLabel": "ビュー: {selectedText}", "xpack.infra.waffle.loadingDataText": "データを読み込み中", "xpack.infra.waffle.maxGroupByTooltip": "一度に選択できるグループは 2 つのみです", - "xpack.infra.waffle.metricButtonLabel": "メトリック: {selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "カウント", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用状況", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "ディスク読み取り", @@ -8246,7 +8225,6 @@ "xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。", "xpack.infra.waffle.noDataTitle": "表示するデータがありません。", "xpack.infra.waffle.region": "すべて", - "xpack.infra.waffle.regionLabel": "地域: {selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "ビューを保存", "xpack.infra.waffle.savedViews.cancel": "キャンセル", "xpack.infra.waffle.savedViews.cancelButton": "キャンセル", @@ -8304,7 +8282,6 @@ "xpack.ingestManager.agentDetails.statusLabel": "ステータス", "xpack.ingestManager.agentDetails.typeLabel": "タイプ", "xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "この構成は利用できなくなりました", - "xpack.ingestManager.agentDetails.unenrollButtonText": "登録解除", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "エージェントを読み込む間にエラーが発生しました", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "ユーザー提供メタデータ", "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "ご希望のエージェント構成とプラットフォームをすばやく選択できます。次いで、以下の手順に従ってエージェントをセットアップして登録します。", @@ -8336,8 +8313,6 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "アクション", "xpack.ingestManager.agentList.actionsMenuText": "開く", "xpack.ingestManager.agentList.addButton": "新しいエージェントをインストール", - "xpack.ingestManager.agentList.agentsOnPageSelectedMessage": "このページで {count, plural, one {# エージェント} other {# エージェント}}が選択されます。{selectAllLink}", - "xpack.ingestManager.agentList.allAgentsSelectedMessage": "{count} エージェントすべてが選択されます。{clearSelectionLink}", "xpack.ingestManager.agentList.clearFiltersLinkText": "フィルターを消去", "xpack.ingestManager.agentList.configColumnTitle": "構成", "xpack.ingestManager.agentList.configFilterText": "構成", @@ -8349,15 +8324,12 @@ "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "エージェントが見つかりません。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "最新ではありません", "xpack.ingestManager.agentList.revisionNumber": "rev. {revNumber}", - "xpack.ingestManager.agentList.selectAllAgentsLinkText": "{count} エージェントすべてを選択", - "xpack.ingestManager.agentList.selectPageAgentsLinkText": "このページのみを選択", "xpack.ingestManager.agentList.showInactiveSwitchLabel": "非アクティブエージェントを表示", "xpack.ingestManager.agentList.statusColumnTitle": "ステータス", "xpack.ingestManager.agentList.statusErrorFilterText": "エラー", "xpack.ingestManager.agentList.statusFilterText": "ステータス", "xpack.ingestManager.agentList.statusOfflineFilterText": "オフライン", "xpack.ingestManager.agentList.statusOnlineFilterText": "オンライン", - "xpack.ingestManager.agentList.unenrollButton": "{count, plural, one {# エージェント} other {# エージェント}} の登録を解除", "xpack.ingestManager.agentList.unenrollOneButton": "登録解除", "xpack.ingestManager.agentList.versionTitle": "バージョン", "xpack.ingestManager.agentList.viewActionText": "エージェントの表示", @@ -8505,10 +8477,7 @@ "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "エージェント「{id}」の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle": "{count} 件のエージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle": "エージェント「{id}」の登録解除エラー", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "エージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle": "{count} 件のエージェントの登録を解除しました", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェント「{id}」の登録を解除しました", "xpack.ingestManager.yamlConfig.instructionDescription": "この構成でエージェントを登録するには、ホストで次のコマンドをコピーして実行します。", "xpack.ingestManager.yamlConfig.instructionTittle": "フリートに登録", @@ -8787,8 +8756,6 @@ "xpack.logstash.idFormatErrorMessage": "パイプライン ID は文字またはアンダーラインで始まる必要があり、文字、アンダーライン、ハイフン、数字のみ使用できます", "xpack.logstash.insufficientUserPermissionsDescription": "Logstash パイプラインの管理に必要なユーザーパーミッションがありません", "xpack.logstash.kibanaManagementPipelinesTitle": "Kibana の管理で作成されたパイプラインだけがここに表示されます", - "xpack.logstash.managementSection.createPipelineTitle": "パイプラインの作成", - "xpack.logstash.managementSection.editPipelineTitle": "パイプラインの編集", "xpack.logstash.managementSection.enableSecurityDescription": "Logstash パイプライン管理機能を使用するには、セキュリティを有効にする必要があります。elasticsearch.yml で xpack.security.enabled: true に設定してください。", "xpack.logstash.managementSection.licenseDoesNotSupportDescription": "ご使用の {licenseType} ライセンスは Logstash パイプライン管理をサポートしていません。ライセンスをアップグレードしてください。", "xpack.logstash.managementSection.notPossibleToManagePipelinesMessage": "現在ライセンス情報が利用できないため Logstash パイプラインを使用できません。", @@ -9473,14 +9440,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分類混同行列", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "予測されたラベル", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "マルチクラス混同行列には、分析が実際のクラスで正しくデータポイントを分類した発生数と、別のクラスで誤分類した発生数が含まれます。", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分類ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "結果が見つかりませんでした。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回帰評価ドキュメント ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "アクションを表示", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "すべての列を表示", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分類ジョブID {jobId}のデスティネーションインデックス", @@ -9555,31 +9516,17 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "データフレーム分析 {jobId} の開始リクエストが受け付けられました。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "トレーニングパーセンテージ", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "機能影響スコア", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "外れ値検出結果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "実験的", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analytics.exploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "外れ値検出ジョブID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", "xpack.ml.dataframe.analytics.exploration.title": "分析の探索", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "予測があるドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回帰ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "{docFieldsCount, number} 件中 showing {selectedFieldsLength, number} 件の{docFieldsCount, plural, one {フィールド} other {フィールド}}", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "回帰分析モデルの実行の効果を測定します。真値と予測値の間の差異の二乗平均合計。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "インデックスのクエリが結果を返しませんでした。デスティネーションインデックスが存在し、ドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "クエリ構文が無効であり、結果を返しませんでした。クエリ構文を確認し、再試行してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "クエリをパースできません。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R の二乗", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "適合度を表します。モデルによる観察された結果の複製の効果を測定します。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回帰ジョブID {jobId}のデスティネーションインデックス", @@ -10924,7 +10871,6 @@ "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "デフォルト", "xpack.monitoring.alerts.licenseExpiration.newSubject": "NEW X-Pack 監視:ライセンス期限", "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "RESOLVED X-Pack 監視:ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは、#relative で #absolute に期限が切れます。", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "このクラスターのライセンスはアクティブです。", "xpack.monitoring.alerts.lowSeverityName": "低", "xpack.monitoring.alerts.mediumSeverityName": "中", @@ -15598,19 +15544,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "インデックスパターン", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "保存検索", "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.pivotPreview.PivotPreviewError": "ピボットプレビューの読み込み中にエラーが発生しました。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "ピボットプレビューを利用できません", "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", "xpack.transform.progress": "進捗", "xpack.transform.sourceIndex": "ソースインデックス", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "ソースインデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列「{columnId}」は並べ替えに使用できません。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "ソースインデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "ソースインデックスクエリの結果がありません", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "ソースインデックスデータの読み込み中にエラーが発生しました。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "ソースインデックス {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "一斉", "xpack.transform.statsBar.continuousTransformsLabel": "連続", "xpack.transform.statsBar.failedTransformsLabel": "失敗", @@ -15699,7 +15637,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "オプションの説明テキストです。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "変換の説明", "xpack.transform.stepDetailsForm.transformIdExistsError": "この ID の変換が既に存在します。", @@ -16740,4 +16677,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5d8d733f2b5b6..1758588d01ba8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -548,6 +548,8 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", "dashboard.factory.displayName": "仪表板", "dashboard.panel.removePanel.replacePanel": "替换面板", + "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", + "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", "data.common.kql.errors.fieldNameText": "字段名称", @@ -669,6 +671,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "此请求将查询 Elasticsearch 以获取用于可视化的数据。", "data.functions.esaggs.inspector.dataRequest.title": "数据", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", @@ -2003,8 +2006,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "以选择有效地定位点文档。", "kbn.context.unableToLoadAnchorDocumentDescription": "无法加载该定位点文档", "kbn.context.unableToLoadDocumentDescription": "无法加载文档", - "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", - "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "kbn.dashboardTitle": "仪表板", "kbn.devToolsTitle": "开发工具", "kbn.discover.backToTopLinkText": "返至顶部。", @@ -2187,10 +2188,8 @@ "kbn.management.createIndexPatternHeader": "创建 {indexPatternName}", "kbn.management.createIndexPatternLabel": "Kibana 使用索引模式从 Elasticsearch 索引中检索数据,以实现诸如可视化等功能。", "kbn.management.editIndexPattern.deleteButton": "删除", - "kbn.management.editIndexPattern.deleteFieldButton": "删除", "kbn.management.editIndexPattern.deleteHeader": "删除索引模式?", "kbn.management.editIndexPattern.detailsAria": "索引模式详细信息", - "kbn.management.editIndexPattern.editFieldButton": "编辑", "kbn.management.editIndexPattern.fields.allLangsDropDown": "所有语言", "kbn.management.editIndexPattern.fields.allTypesDropDown": "所有字段类型", "kbn.management.editIndexPattern.fields.filterAria": "筛选", @@ -2216,7 +2215,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "类型", "kbn.management.editIndexPattern.mappingConflictHeader": "映射冲突", "kbn.management.editIndexPattern.mappingConflictLabel": "匹配此模式的各个索引中{conflictFieldsLength, plural, one {一个字段已} other {# 个字段已}}定义为若干类型(字符串、整数等)。您仍能够在 Kibana 的各个部分中使用这些冲突类型,但它们将无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。", - "kbn.management.editIndexPattern.notDateErrorMessage": "该字段是{fieldType},不是日期。", "kbn.management.editIndexPattern.refreshAria": "重新加载字段列表", "kbn.management.editIndexPattern.refreshButton": "刷新", "kbn.management.editIndexPattern.refreshHeader": "刷新字段列表?", @@ -2296,6 +2294,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "应用的完整列表位于左侧菜单中。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可视化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", @@ -2417,8 +2417,6 @@ "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", "savedObjectsManagement.breadcrumb.edit": "编辑 {savedObjectType}", "savedObjectsManagement.breadcrumb.index": "已保存对象", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可视化", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "kibana_legacy.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", @@ -2432,7 +2430,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "全部", "kibana_utils.defaultFeedbackMessage": "想反馈?请在 {link} 中创建问题。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "已保存对象缺失", - "kibana_utils.indexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完全还原 URL,请确保使用共享功能。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,另外,似乎没有任何可安全删除的项目。\n\n通常,这可以通过移到全新的选项卡来解决,但这种情况可能是由更大的问题造成。如果您定期看到这个消息,请在 {gitHubIssuesUrl} 报告问题。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", @@ -5022,8 +5019,6 @@ "xpack.canvas.elements.bubbleChartHelpText": "可定制的气泡图", "xpack.canvas.elements.debugDisplayName": "“Debug”(故障排查)", "xpack.canvas.elements.debugHelpText": "只需丢弃元素的配置", - "xpack.canvas.elements.donutChartDisplayName": "圆环图", - "xpack.canvas.elements.donutChartHelpText": "可定制的圆环图", "xpack.canvas.elements.dropdownFilterDisplayName": "下拉列表筛选", "xpack.canvas.elements.dropdownFilterHelpText": "可以从其中为“完全”筛选选择值的下拉列表", "xpack.canvas.elements.horizontalBarChartDisplayName": "水平条形图", @@ -5058,8 +5053,6 @@ "xpack.canvas.elements.shapeHelpText": "可定制的形状", "xpack.canvas.elements.tableDisplayName": "数据表", "xpack.canvas.elements.tableHelpText": "用于以表格形式显示数据的可滚动网格", - "xpack.canvas.elements.tiltedPieDisplayName": "斜饼图", - "xpack.canvas.elements.tiltedPieHelpText": "可定制的斜饼图", "xpack.canvas.elements.timeFilterDisplayName": "时间筛选", "xpack.canvas.elements.timeFilterHelpText": "设置时间窗口", "xpack.canvas.elements.verticalBarChartDisplayName": "垂直条形图", @@ -5070,16 +5063,16 @@ "xpack.canvas.elements.verticalProgressPillHelpText": "将进度显示为垂直胶囊的一部分", "xpack.canvas.elementSettings.dataTabLabel": "数据", "xpack.canvas.elementSettings.displayTabLabel": "显示", - "xpack.canvas.elementTypes.addNewElementDescription": "分组并保存 Workpad 元素以创建新元素", - "xpack.canvas.elementTypes.addNewElementTitle": "添加新元素", - "xpack.canvas.elementTypes.cancelButtonLabel": "取消", - "xpack.canvas.elementTypes.deleteButtonLabel": "删除", - "xpack.canvas.elementTypes.deleteElementDescription": "确定要删除此元素?", - "xpack.canvas.elementTypes.deleteElementTitle": "删除元素“{elementName}”?", - "xpack.canvas.elementTypes.editElementTitle": "编辑元素", - "xpack.canvas.elementTypes.elementsTitle": "元素", - "xpack.canvas.elementTypes.findElementPlaceholder": "查找元素", - "xpack.canvas.elementTypes.myElementsTitle": "我的元素", + "xpack.canvas.savedElementsModal.addNewElementDescription": "分组并保存 Workpad 元素以创建新元素", + "xpack.canvas.savedElementsModal.addNewElementTitle": "添加新元素", + "xpack.canvas.savedElementsModal.cancelButtonLabel": "取消", + "xpack.canvas.savedElementsModal.deleteButtonLabel": "删除", + "xpack.canvas.savedElementsModal.deleteElementDescription": "确定要删除此元素?", + "xpack.canvas.savedElementsModal.deleteElementTitle": "删除元素“{elementName}”?", + "xpack.canvas.savedElementsModal.editElementTitle": "编辑元素", + "xpack.canvas.savedElementsModal.elementsTitle": "元素", + "xpack.canvas.savedElementsModal.findElementPlaceholder": "查找元素", + "xpack.canvas.savedElementsModal.myElementsTitle": "我的元素", "xpack.canvas.embedObject.noMatchingObjectsMessage": "未找到任何匹配对象。", "xpack.canvas.embedObject.titleText": "嵌入对象", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "无效的参数索引:{index}", @@ -5592,13 +5585,8 @@ "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "上", "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "取消分组", "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "垂直", - "xpack.canvas.tags.chartTag": "图表", - "xpack.canvas.tags.filterTag": "筛选", - "xpack.canvas.tags.graphicTag": "图形", "xpack.canvas.tags.presentationTag": "演示", - "xpack.canvas.tags.proportionTag": "比例", "xpack.canvas.tags.reportTag": "报告", - "xpack.canvas.tags.textTag": "文本", "xpack.canvas.templates.darkHelp": "深色主题的演示幻灯片", "xpack.canvas.templates.darkName": "深色", "xpack.canvas.templates.lightHelp": "浅色主题的演示幻灯片", @@ -5899,7 +5887,6 @@ "xpack.canvas.workpadHeader.cycleIntervalHoursText": "每 {hours} {hours, plural, one {小时} other {小时}}", "xpack.canvas.workpadHeader.cycleIntervalMinutesText": "每 {minutes} {minutes, plural, one {分钟} other {分钟}}", "xpack.canvas.workpadHeader.cycleIntervalSecondsText": "每 {seconds} {seconds, plural, one {秒} other {秒}}", - "xpack.canvas.workpadHeader.embedObjectButtonLabel": "嵌入对象", "xpack.canvas.workpadHeader.fullscreenButtonAriaLabel": "全屏查看", "xpack.canvas.workpadHeader.fullscreenTooltip": "进入全屏模式", "xpack.canvas.workpadHeader.hideEditControlTooltip": "隐藏编辑控件", @@ -5909,7 +5896,6 @@ "xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel": "更改自动刷新时间间隔", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText": "手动", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle": "刷新元素", - "xpack.canvas.workpadHeaderControlSettings.settingsTooltip": "控制设置", "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "设置", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "使用速记表示法,如 {secondsExample}、{minutesExample} 或 {hoursExample}", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "设置定制时间间隔", @@ -5918,32 +5904,32 @@ "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "自动循环播放幻灯片", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "刷新元素", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "刷新数据", - "xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage": "{PDF} 生成 {URL} 已复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage": "已将报告配置复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage": "已将共享标记复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage": "无法为“{workpadName}”创建 {PDF}", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage": "正在导出 {PDF}。可以在“管理”中跟踪进度。", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle": "Workpad“{workpadName}”的 {PDF} 导出", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel": "或者,也可以从脚本或使用 {URL} 通过 Watcher 生成 {PDF}。按 Enter 键可将 {URL} 复制到剪贴板。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel": "复制 {POST} {URL}", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription": "或者,复制此 {POST} {URL} 以从 {KIBANA} 外部或从 Watcher 调用生成。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel": "生成 {PDF}", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription": "{PDF} 可能会花费 1 或 2 分钟生成,取决于 Workpad 的大小。", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle": "下载为 {JSON}", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle": "{PDF} 报告", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle": "无法为“{workpadName}”创建 {ZIP} 文件。Workpad 可能过大。您将需要分别下载文件。", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle": "在网站上共享", - "xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage": "共享此 Workpad", - "xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage": "未知导出类型:{type}", - "xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning": "此 Workpad 包含 {CANVAS} Shareable Workpad Runtime 不支持的呈现函数。将不会呈现以下元素:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel": "缩放控制", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip": "缩放控制", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText": "适应窗口大小", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomInText": "放大", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText": "缩小", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle": "缩放", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue": "重置", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText": "{scalePercentage}%", + "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} 已复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "已将报告配置复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "已将共享标记复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "无法为“{workpadName}”创建 {PDF}", + "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "正在导出 {PDF}。可以在“管理”中跟踪进度。", + "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "Workpad“{workpadName}”的 {PDF} 导出", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "或者,也可以从脚本或使用 {URL} 通过 Watcher 生成 {PDF}。按 Enter 键可将 {URL} 复制到剪贴板。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "复制 {POST} {URL}", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "或者,复制此 {POST} {URL} 以从 {KIBANA} 外部或从 Watcher 调用生成。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "生成 {PDF}", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "{PDF} 可能会花费 1 或 2 分钟生成,取决于 Workpad 的大小。", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "下载为 {JSON}", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} 报告", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle": "无法为“{workpadName}”创建 {ZIP} 文件。Workpad 可能过大。您将需要分别下载文件。", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle": "在网站上共享", + "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "共享此 Workpad", + "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "未知导出类型:{type}", + "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "此 Workpad 包含 {CANVAS} Shareable Workpad Runtime 不支持的呈现函数。将不会呈现以下元素:", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "缩放控制", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "缩放控制", + "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "适应窗口大小", + "xpack.canvas.workpadHeaderViewMenu.zoomInText": "放大", + "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "缩小", + "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放", + "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置", + "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} 的副本", "xpack.canvas.workpadLoader.cloneTooltip": "克隆 Workpad", "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "正在创建 Workpad......", @@ -6100,9 +6086,6 @@ "xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "已暂停", "xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "状态", "xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "Follower 索引后缀", - "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期", - "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", - "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "取消", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "删除", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "是否删除 {count} 个自动跟随模式?", @@ -8182,7 +8165,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "地图视图", "xpack.infra.viewSwitcher.tableViewLabel": "表视图", "xpack.infra.waffle.accountAllTitle": "全部", - "xpack.infra.waffle.accountLabel": "帐户:{selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "“{field}”的平均值", "xpack.infra.waffle.aggregationNames.max": "“{field}”的最大值", "xpack.infra.waffle.aggregationNames.min": "“{field}”的最小值", @@ -8218,11 +8200,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "保存定制指标的更改", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "全部", - "xpack.infra.waffle.groupByButtonLabel": "分组依据: ", - "xpack.infra.waffle.inventoryButtonLabel": "视图:{selectedText}", "xpack.infra.waffle.loadingDataText": "正在加载数据", "xpack.infra.waffle.maxGroupByTooltip": "一次只能选择两个分组", - "xpack.infra.waffle.metricButtonLabel": "指标:{selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "计数", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取", @@ -8249,7 +8228,6 @@ "xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。", "xpack.infra.waffle.noDataTitle": "没有可显示的数据。", "xpack.infra.waffle.region": "全部", - "xpack.infra.waffle.regionLabel": "地区:{selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "保存视图", "xpack.infra.waffle.savedViews.cancel": "取消", "xpack.infra.waffle.savedViews.cancelButton": "取消", @@ -8307,7 +8285,6 @@ "xpack.ingestManager.agentDetails.statusLabel": "状态", "xpack.ingestManager.agentDetails.typeLabel": "类型", "xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "此配置不再可用", - "xpack.ingestManager.agentDetails.unenrollButtonText": "取消注册", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "加载代理时发生错误", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "用户提供的元数据", "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "快速选择所需的代理配置和平台。然后,根据下面的说明设置和注册代理。", @@ -8339,8 +8316,6 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "操作", "xpack.ingestManager.agentList.actionsMenuText": "打开", "xpack.ingestManager.agentList.addButton": "安装新代理", - "xpack.ingestManager.agentList.agentsOnPageSelectedMessage": "已选择此页面上的 {count, plural, one {# 个代理} other {# 个代理}}。{selectAllLink}", - "xpack.ingestManager.agentList.allAgentsSelectedMessage": "已选择所有 {count} 个代理。{clearSelectionLink}", "xpack.ingestManager.agentList.clearFiltersLinkText": "清除筛选", "xpack.ingestManager.agentList.configColumnTitle": "配置", "xpack.ingestManager.agentList.configFilterText": "配置", @@ -8352,15 +8327,12 @@ "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "未找到任何代理。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "过时", "xpack.ingestManager.agentList.revisionNumber": "修订 {revNumber}", - "xpack.ingestManager.agentList.selectAllAgentsLinkText": "选择所有 {count} 个代理", - "xpack.ingestManager.agentList.selectPageAgentsLinkText": "仅选择此页面", "xpack.ingestManager.agentList.showInactiveSwitchLabel": "显示非活动代理", "xpack.ingestManager.agentList.statusColumnTitle": "状态", "xpack.ingestManager.agentList.statusErrorFilterText": "错误", "xpack.ingestManager.agentList.statusFilterText": "状态", "xpack.ingestManager.agentList.statusOfflineFilterText": "脱机", "xpack.ingestManager.agentList.statusOnlineFilterText": "联机", - "xpack.ingestManager.agentList.unenrollButton": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}", "xpack.ingestManager.agentList.unenrollOneButton": "取消注册", "xpack.ingestManager.agentList.versionTitle": "版本", "xpack.ingestManager.agentList.viewActionText": "查看代理", @@ -8508,10 +8480,7 @@ "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "取消注册“{id}”?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle": "取消注册 {count} 个代理时出错", - "xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle": "取消注册代理“{id}”时出错", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册代理时出错", - "xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle": "已取消注册 {count} 个代理", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "已取消注册代理“{id}”", "xpack.ingestManager.yamlConfig.instructionDescription": "要将代理注册到此配置,请在您的主机上复制并运行以下命令。", "xpack.ingestManager.yamlConfig.instructionTittle": "注册到 fleet", @@ -8790,8 +8759,6 @@ "xpack.logstash.idFormatErrorMessage": "管道 ID 必须以字母或下划线开头,并只能包含字母、下划线、短划线和数字", "xpack.logstash.insufficientUserPermissionsDescription": "管理 Logstash 管道的用户权限不足", "xpack.logstash.kibanaManagementPipelinesTitle": "仅在 Kibana“管理”中创建的管道显示在此处", - "xpack.logstash.managementSection.createPipelineTitle": "创建管道", - "xpack.logstash.managementSection.editPipelineTitle": "编辑管道", "xpack.logstash.managementSection.enableSecurityDescription": "必须启用 Security,才能使用 Logstash 管道管理功能。请在 elasticsearch.yml 中设置 xpack.security.enabled: true。", "xpack.logstash.managementSection.licenseDoesNotSupportDescription": "您的{licenseType}许可不支持 Logstash 管道管理功能。请升级您的许可。", "xpack.logstash.managementSection.notPossibleToManagePipelinesMessage": "您不能管理 Logstash 管道,因为许可信息当前不可用。", @@ -9476,14 +9443,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分类混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "预测标签", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "多类混淆矩阵包含分析使用数据点的实际类正确分类数据点的次数以及分析使用其他类错误分类这些数据点的次数", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分类作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "未找到结果。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回归评估文档 ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "显示操作", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "显示所有列", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分类作业 ID {jobId} 的目标索引", @@ -9558,31 +9519,17 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "数据帧分析 {jobId} 启动请求已确认。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "功能影响分数", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "离群值检测结果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "实验性", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analytics.exploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "离群值检测作业 ID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "该索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回归作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "已选择 {docFieldsCount, number} 个{docFieldsCount, plural, one {字段} other {字段}}中的 {selectedFieldsLength, number} 个", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "度量回归分析模型的表现。真实值与预测值之差的平均平方和。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "该索引的查询未返回结果。请确保目标索引存在且包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "查询语法无效,未返回任何结果。请检查查询语法并重试。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "无法解析查询。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R 平方", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "表示拟合优度。度量模型复制被观察结果的优良性。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回归作业 ID {jobId} 的目标索引", @@ -10928,7 +10875,6 @@ "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "默认值", "xpack.monitoring.alerts.licenseExpiration.newSubject": "新 X-Pack Monitoring:许可证到期", "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "已解决 X-Pack Monitoring:许可证到期", - "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将在 #relative 后,即 #absolute到期", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "此集群的许可证处于活动状态。", "xpack.monitoring.alerts.lowSeverityName": "低", "xpack.monitoring.alerts.mediumSeverityName": "中", @@ -15602,19 +15548,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "索引模式", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "已保存搜索", "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", - "xpack.transform.pivotPreview.PivotPreviewError": "加载数据透视表预览时出错。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "数据透视表预览不可用", "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", "xpack.transform.progress": "进度", "xpack.transform.sourceIndex": "源索引", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "将源索引预览的开发控制台语句复制到剪贴板。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列“{columnId}”无法用于排序。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "源索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "源索引查询结果为空。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "加载源索引数据时出错。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "源索引 {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "批量", "xpack.transform.statsBar.continuousTransformsLabel": "连续", "xpack.transform.statsBar.failedTransformsLabel": "失败", @@ -15703,7 +15641,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "(可选)描述性文本。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "转换描述", "xpack.transform.stepDetailsForm.transformIdExistsError": "已存在具有此 ID 的转换。", @@ -16745,4 +16682,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx index 658a0e869548f..fdfaf70648694 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx @@ -4,12 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { FunctionComponent } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from 'react-dom/test-utils'; import { TypeRegistry } from '../../type_registry'; import { registerBuiltInActionTypes } from './index'; import { ActionTypeModel, ActionParamsProps } from '../../../types'; import { IndexActionParams, EsIndexActionConnector } from './types'; import { coreMock } from '../../../../../../../src/core/public/mocks'; +jest.mock('../../../common/index_controls', () => ({ + firstFieldOption: jest.fn(), + getFields: jest.fn(), + getIndexOptions: jest.fn(), + getIndexPatterns: jest.fn(), +})); const ACTION_TYPE_ID = '.index'; let actionTypeModel: ActionTypeModel; @@ -91,13 +98,40 @@ describe('action params validation', () => { }); describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { + test('all connector fields is rendered', async () => { const mocks = coreMock.createSetup(); expect(actionTypeModel.actionConnectorFields).not.toBeNull(); if (!actionTypeModel.actionConnectorFields) { return; } + + const { getIndexPatterns } = jest.requireMock('../../../common/index_controls'); + getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, + ]); + const { getFields } = jest.requireMock('../../../common/index_controls'); + getFields.mockResolvedValueOnce([ + { + type: 'date', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); const ConnectorFields = actionTypeModel.actionConnectorFields; const actionConnector = { secrets: {}, @@ -119,8 +153,38 @@ describe('IndexActionConnectorFields renders', () => { http={mocks.http} /> ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + + const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); + expect(indexSearchBoxValue.first().props().value).toEqual(''); + + const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); + indexComboBox.first().simulate('click'); + const event = { target: { value: 'indexPattern1' } }; + indexComboBox + .find('input') + .first() + .simulate('change', event); + + const indexSearchBoxValueBeforeEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + + const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); + indexComboBoxClear.first().simulate('click'); + + const indexSearchBoxValueAfterEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 15f68e6a9f441..55a219ca94aea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -151,7 +151,7 @@ const IndexActionConnectorFields: React.FunctionComponent { - editActionConfig('index', selected[0].value); + editActionConfig('index', selected.length > 0 ? selected[0].value : ''); const indices = selected.map(s => s.value as string); // reset time field and expression fields if indices are deleted diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx index 7da97b9fe3436..1c9e87310107f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx @@ -90,7 +90,7 @@ describe('pagerduty action params validation', () => { summary: '2323', source: 'source', severity: 'critical', - timestamp: '234654564654', + timestamp: new Date().toISOString(), component: 'test', group: 'group', class: 'test class', @@ -99,6 +99,7 @@ describe('pagerduty action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { summary: [], + timestamp: [], }, }); }); @@ -156,7 +157,7 @@ describe('PagerDutyParamsFields renders', () => { summary: '2323', source: 'source', severity: SeverityActionOptions.CRITICAL, - timestamp: '234654564654', + timestamp: new Date().toISOString(), component: 'test', group: 'group', class: 'test class', @@ -164,7 +165,7 @@ describe('PagerDutyParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx index d99362c618356..15f91ae1d4609 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx @@ -14,6 +14,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; import { ActionTypeModel, ActionConnectorFieldsProps, @@ -23,6 +24,7 @@ import { import { PagerDutyActionParams, PagerDutyActionConnector } from './types'; import pagerDutySvg from './pagerduty.svg'; import { AddMessageVariables } from '../add_message_variables'; +import { hasMustacheTokens } from '../../lib/has_mustache_tokens'; export function getActionType(): ActionTypeModel { return { @@ -62,6 +64,7 @@ export function getActionType(): ActionTypeModel { const validationResult = { errors: {} }; const errors = { summary: new Array(), + timestamp: new Array(), }; validationResult.errors = errors; if (!actionParams.summary?.length) { @@ -74,6 +77,24 @@ export function getActionType(): ActionTypeModel { ) ); } + if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) { + if (isNaN(Date.parse(actionParams.timestamp))) { + const { nowShortFormat, nowLongFormat } = getValidTimestampExamples(); + errors.timestamp.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp', + { + defaultMessage: + 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.', + values: { + nowShortFormat, + nowLongFormat, + }, + } + ) + ); + } + } return validationResult; }, actionConnectorFields: PagerDutyActionConnectorFields, @@ -334,6 +355,8 @@ const PagerDutyParamsFields: React.FunctionComponent 0 && timestamp !== undefined} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel', { @@ -355,11 +378,14 @@ const PagerDutyParamsFields: React.FunctionComponent 0 && timestamp !== undefined} onChange={(e: React.ChangeEvent) => { editAction('timestamp', e.target.value, index); }} onBlur={() => { - if (!timestamp) { + if (timestamp?.trim()) { + editAction('timestamp', timestamp.trim(), index); + } else { editAction('timestamp', '', index); } }} @@ -534,3 +560,11 @@ const PagerDutyParamsFields: React.FunctionComponent ); }; + +function getValidTimestampExamples() { + const now = moment(); + return { + nowShortFormat: now.format('YYYY-MM-DD'), + nowLongFormat: now.format('YYYY-MM-DD h:mm:ss'), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.test.ts new file mode 100644 index 0000000000000..db4f9fa799170 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.test.ts @@ -0,0 +1,23 @@ +/* + * 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 uuid from 'uuid'; + +import { hasMustacheTokens } from './has_mustache_tokens'; + +describe('hasMustacheTokens', () => { + test('returns false for empty string', () => { + expect(hasMustacheTokens('')).toBe(false); + }); + + test('returns false for string without tokens', () => { + expect(hasMustacheTokens(`some random string ${uuid.v4()}`)).toBe(false); + }); + + test('returns true when a template token is present', () => { + expect(hasMustacheTokens('{{context.timestamp}}')).toBe(true); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts new file mode 100644 index 0000000000000..4dcd8113d51fc --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function hasMustacheTokens(str: string): boolean { + return null !== str.match(/{{.*}}/); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx index 2f3c0000ef96b..b01104a8d5cf7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx @@ -45,6 +45,7 @@ describe('of expression', () => { "asPlainText": true, } } + sortMatchesBy="none" /> `); }); @@ -105,6 +106,7 @@ describe('of expression', () => { "asPlainText": true, } } + sortMatchesBy="none" /> `); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts index 4228426d62159..5301f6364529d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts @@ -6,7 +6,7 @@ import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; -import { httpServiceMock } from 'src/core/public/http/http_service.mock'; +import { httpServiceMock } from 'src/core/public/mocks'; const mockClient = httpServiceMock.createSetupContract(); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 2af1a9ac38e44..24347b7799871 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -51,6 +51,7 @@ describe('Upgrade Assistant Usage Collector', () => { 'ui_reindex.open': 4, 'ui_reindex.start': 2, 'ui_reindex.stop': 1, + 'ui_reindex.not_defined': 1, }, }; }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 9c2946db7f084..0c2e3a1e43f4a 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { get } from 'lodash'; import { APICaller, ElasticsearchServiceStart, @@ -84,16 +84,19 @@ export async function fetchUpgradeAssistantMetrics( return defaultTelemetrySavedObject; } - const upgradeAssistantTelemetrySOAttrsKeys = Object.keys( - upgradeAssistantTelemetrySavedObjectAttrs - ); - const telemetryObj = defaultTelemetrySavedObject; - - upgradeAssistantTelemetrySOAttrsKeys.forEach((key: string) => { - set(telemetryObj, key, upgradeAssistantTelemetrySavedObjectAttrs[key]); - }); - - return telemetryObj as UpgradeAssistantTelemetrySavedObject; + return { + ui_open: { + overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0), + cluster: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.cluster', 0), + indices: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.indices', 0), + }, + ui_reindex: { + close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0), + open: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.open', 0), + start: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.start', 0), + stop: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.stop', 0), + }, + } as UpgradeAssistantTelemetrySavedObject; }; return { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index b2b8ccf1ca57a..78b03275e0ef9 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -5,7 +5,7 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; +import { savedObjectsServiceMock } from 'src/core/server/mocks'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 2cc6f23ebaae5..4f4c6e3011ad1 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -16,7 +16,7 @@ import { AlertType } from '../../../../../alerting/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; /** @@ -52,7 +52,7 @@ const mockOptions = ( id: '', type: '', references: [], - attributes: defaultDynamicSettings, + attributes: DYNAMIC_SETTINGS_DEFAULTS, }); return { params, @@ -88,9 +88,9 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 30, + "certThresholds": Object { + "age": 365, + "expiration": 30, }, "heartbeatIndices": "heartbeat-8*", }, @@ -135,9 +135,9 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 30, + "certThresholds": Object { + "age": 365, + "expiration": 30, }, "heartbeatIndices": "heartbeat-8*", }, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index 539344dfca791..b49a6b22ff976 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -5,6 +5,7 @@ */ import { getCerts } from '../get_certs'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getCerts', () => { let mockHits: any; @@ -86,7 +87,10 @@ describe('getCerts', () => { it('parses query result and returns expected values', async () => { const result = await getCerts({ callES: mockCallES, - dynamicSettings: { heartbeatIndices: 'heartbeat*' }, + dynamicSettings: { + heartbeatIndices: 'heartbeat*', + certThresholds: DYNAMIC_SETTINGS_DEFAULTS.certThresholds, + }, index: 1, from: 'now-2d', to: 'now+1h', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index cf8414a3b0a68..03e2bc7a44bd0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -5,14 +5,14 @@ */ import { getLatestMonitor } from '../get_latest_monitor'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getLatestMonitor', () => { let expectedGetLatestSearchParams: any; let mockEsSearchResult: any; beforeEach(() => { expectedGetLatestSearchParams = { - index: defaultDynamicSettings.heartbeatIndices, + index: DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices, body: { query: { bool: { @@ -64,7 +64,7 @@ describe('getLatestMonitor', () => { const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult); const result = await getLatestMonitor({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateStart: 'now-1h', dateEnd: 'now', monitorId: 'testMonitor', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index c740581734fdd..5d3f9ce8d4ad9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -7,7 +7,7 @@ import { set } from 'lodash'; import mockChartsData from './monitor_charts_mock.json'; import { getMonitorDurationChart } from '../get_monitor_duration'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will provide expected filters', async () => { @@ -16,7 +16,7 @@ describe('ElasticsearchMonitorsAdapter', () => { const search = searchMock.bind({}); await getMonitorDurationChart({ callES: search, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, monitorId: 'fooID', dateStart: 'now-15m', dateEnd: 'now', @@ -39,7 +39,7 @@ describe('ElasticsearchMonitorsAdapter', () => { expect( await getMonitorDurationChart({ callES: search, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, monitorId: 'id', dateStart: 'now-15m', dateEnd: 'now', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index e429de9ae0d68..e47be617d7c99 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -6,8 +6,8 @@ import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; -import { ScopedClusterClient } from 'src/core/server/elasticsearch'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { ScopedClusterClient } from 'src/core/server'; interface BucketItemCriteria { monitor_id: string; @@ -103,7 +103,7 @@ describe('getMonitorStatus', () => { }`; await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, filters: exampleFilter, locations: [], numTimes: 5, @@ -206,7 +206,7 @@ describe('getMonitorStatus', () => { const [callES, esMock] = setupMock([]); await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, locations: ['fairbanks', 'harrisburg'], numTimes: 1, timerange: { @@ -329,7 +329,7 @@ describe('getMonitorStatus', () => { }; const result = await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, ...clientParameters, }); expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); @@ -494,7 +494,7 @@ describe('getMonitorStatus', () => { const [callES] = setupMock(criteria); const result = await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, locations: [], numTimes: 5, timerange: { 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 faeb291bb533b..4de7d3ffd2a7d 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 @@ -5,7 +5,7 @@ */ import { getPingHistogram } from '../get_ping_histogram'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getPingHistogram', () => { const standardMockResponse: any = { @@ -59,7 +59,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters: null, @@ -78,7 +78,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters: null, @@ -140,7 +140,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: JSON.stringify(searchFilter), @@ -196,7 +196,7 @@ describe('getPingHistogram', () => { const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`; const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters, @@ -213,7 +213,7 @@ describe('getPingHistogram', () => { mockEsClient.mockReturnValue(standardMockResponse); const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: '', @@ -234,7 +234,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: '', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index fcf773db23de6..abd3655cc6402 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -6,7 +6,7 @@ import { getPings } from '../get_pings'; import { set } from 'lodash'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getAll', () => { let mockEsSearchResult: any; @@ -62,7 +62,7 @@ describe('getAll', () => { }, }; expectedGetAllParams = { - index: defaultDynamicSettings.heartbeatIndices, + index: DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices, body: { query: { bool: { @@ -88,7 +88,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); const result = await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -110,7 +110,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -166,7 +166,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, size: 12, }); @@ -220,7 +220,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'desc', }); @@ -274,7 +274,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, monitorId: 'testmonitorid', }); @@ -333,7 +333,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, status: 'down', }); diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 3ccfd498c44bf..d849fbd8ce0a8 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - DynamicSettings, - defaultDynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; @@ -28,12 +26,12 @@ export const umDynamicSettings: SavedObjectsType = { heartbeatIndices: { type: 'keyword', }, - certificatesThresholds: { + certThresholds: { properties: { - errorState: { + expiration: { type: 'long', }, - warningState: { + age: { type: 'long', }, }, @@ -49,7 +47,7 @@ export const savedObjectsAdapter: UMSavedObjectsAdapter = { return obj.attributes; } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { - return defaultDynamicSettings; + return DYNAMIC_SETTINGS_DEFAULTS; } throw getErr; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index c84b089d48c85..e43f9dba4b2dc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -141,9 +141,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { actionTypeId: '.slack', name: 'Slack#xyz', isPreconfigured: true, - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 0b637326d4667..95b564e63d715 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -73,11 +73,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -85,9 +80,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -95,11 +87,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -107,9 +94,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -194,11 +178,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -206,9 +185,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 1, }, { @@ -216,11 +192,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -228,9 +199,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -281,11 +249,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -293,9 +256,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -303,11 +263,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -315,9 +270,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index a4a13441fb766..4eb8c16f4fb3a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -79,9 +79,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index ec59e56b08308..62abdddc6a1bf 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -50,11 +50,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -62,9 +57,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -72,11 +64,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -84,9 +71,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -115,11 +99,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -127,9 +106,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -137,11 +113,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -149,9 +120,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts index 41d78995711f2..8af648e062cf4 100644 --- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -182,15 +182,21 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'development' }, settings: { transaction_sample_rate: '0.9' }, }; + const configProduction = { + service: { name: 'myservice', environment: 'production' }, + settings: { transaction_sample_rate: '0.9' }, + }; let etag: string; before(async () => { log.debug('creating agent configuration'); await createConfiguration(config); + await createConfiguration(configProduction); }); after(async () => { await deleteConfiguration(config); + await deleteConfiguration(configProduction); }); it(`should have 'applied_by_agent=false' before supplying etag`, async () => { @@ -210,17 +216,45 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); it(`should have 'applied_by_agent=true' after supplying etag`, async () => { - async function getAppliedByAgent() { + await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag, + }); + + async function hasBeenAppliedByAgent() { const { body } = await searchConfigurations({ service: { name: 'myservice', environment: 'development' }, - etag, }); return body._source.applied_by_agent; } // wait until `applied_by_agent` has been updated in elasticsearch - expect(await waitFor(getAppliedByAgent)).to.be(true); + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); + }); + it(`should have 'applied_by_agent=false' before marking as applied`, async () => { + const res1 = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + expect(res1.body._source.applied_by_agent).to.be(false); + }); + it(`should have 'applied_by_agent=true' when 'mark_as_applied_by_agent' attribute is true`, async () => { + await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + mark_as_applied_by_agent: true, + }); + + async function hasBeenAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); }); }); }); diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index b484f1f5a8ed2..d33b92acf95a5 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -44,6 +44,7 @@ export default function(providerContext: FtrProviderContext) { }); // @ts-ignore agentDoc.agents.access_api_key_id = accessAPIKeyId; + agentDoc.agents.default_api_key_id = outputAPIKeyBody.id; agentDoc.agents.default_api_key = Buffer.from( `${outputAPIKeyBody.id}:${outputAPIKeyBody.api_key}` ).toString('base64'); @@ -61,50 +62,29 @@ export default function(providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); - it('should not allow both ids and kuery in the payload', async () => { - await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({ - ids: ['agent:1'], - kuery: ['agents.id:1'], - }) - .expect(400); - }); - - it('should not allow no ids or kuery in the payload', async () => { - await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({}) - .expect(400); - }); - it('allow to unenroll using a list of ids', async () => { const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) + .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ ids: ['agent1'], }) .expect(200); - expect(body).to.have.keys('results', 'success'); + expect(body).to.have.keys('success'); expect(body.success).to.be(true); - expect(body.results).to.have.length(1); - expect(body.results[0].success).to.be(true); }); it('should invalidate related API keys', async () => { const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) + .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ ids: ['agent1'], }) .expect(200); - expect(body).to.have.keys('results', 'success'); + expect(body).to.have.keys('success'); expect(body.success).to.be(true); const { @@ -119,25 +99,5 @@ export default function(providerContext: FtrProviderContext) { expect(outputAPIKeys).length(1); expect(outputAPIKeys[0].invalidated).eql(true); }); - - it('allow to unenroll using a kibana query', async () => { - const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({ - kuery: 'agents.shared_id:agent2_filebeat OR agents.shared_id:agent3_metricbeat', - }) - .expect(200); - - expect(body).to.have.keys('results', 'success'); - expect(body.success).to.be(true); - expect(body.results).to.have.length(2); - expect(body.results[0].success).to.be(true); - - const agentsUnenrolledIds = body.results.map((r: { id: string }) => r.id); - - expect(agentsUnenrolledIds).to.contain('agent2'); - expect(agentsUnenrolledIds).to.contain('agent3'); - }); }); } diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts index cdbf5a3e6a1fe..2463dbe4500b5 100644 --- a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts +++ b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts @@ -20,7 +20,6 @@ export default function({ getService }: FtrProviderContext) { .send({ id: 'fast_generator', description: 'foobar baz', - username: 'seger', pipeline: 'input { generator {} }\n\n output { stdout {} }', }) .expect(204); diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts index 2ca9fbe7d68e0..ca0cfb19b9454 100644 --- a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts +++ b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts @@ -28,7 +28,6 @@ export default function({ getService }: FtrProviderContext) { .send({ id: 'fast_generator', description: 'foobar baz', - username: 'seger', pipeline: 'input { generator {} }\n\n output { stdout {} }', }) .expect(204); diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js index 22f0bde50b073..b9a0bfd40a8d6 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js @@ -5,33 +5,27 @@ */ import { API_BASE_PATH } from './constants'; -import { getRandomString } from './lib'; -import { getAutoFollowIndexPayload } from './fixtures'; export const registerHelpers = supertest => { let autoFollowPatternsCreated = []; const loadAutoFollowPatterns = () => supertest.get(`${API_BASE_PATH}/auto_follow_patterns`); - const getAutoFollowPattern = name => - supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${name}`); + const getAutoFollowPattern = id => supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${id}`); - const createAutoFollowPattern = ( - name = getRandomString(), - payload = getAutoFollowIndexPayload() - ) => { - autoFollowPatternsCreated.push(name); + const createAutoFollowPattern = payload => { + autoFollowPatternsCreated.push(payload.id); return supertest .post(`${API_BASE_PATH}/auto_follow_patterns`) .set('kbn-xsrf', 'xxx') - .send({ ...payload, id: name }); + .send(payload); }; - const deleteAutoFollowPattern = name => { - autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== name); + const deleteAutoFollowPattern = id => { + autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== id); - return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${name}`).set('kbn-xsrf', 'xxx'); + return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${id}`).set('kbn-xsrf', 'xxx'); }; const deleteAllAutoFollowPatterns = () => diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js index 3efb4d6600f7f..7a95ba7fcd981 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js @@ -6,8 +6,7 @@ import expect from '@kbn/expect'; -import { getRandomString } from './lib'; -import { getAutoFollowIndexPayload } from './fixtures'; +import { REMOTE_CLUSTER_NAME } from './constants'; import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers'; import { registerHelpers as registerAutoFollowPatternHelpers } from './auto_follow_pattern.helpers'; @@ -37,44 +36,60 @@ export default function({ getService }) { describe('when remote cluster does not exist', () => { it('should throw a 404 error when cluster is unknown', async () => { - const payload = getAutoFollowIndexPayload(); - payload.remoteCluster = 'unknown-cluster'; + const { body } = await createAutoFollowPattern({ + id: 'pattern0', + remoteCluster: 'unknown-cluster', + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); - const { body } = await createAutoFollowPattern(undefined, payload).expect(404); + expect(body.statusCode).to.be(404); expect(body.attributes.cause[0]).to.contain('no such remote cluster'); }); }); describe('when remote cluster exists', () => { - before(() => addCluster()); + before(async () => addCluster()); describe('create()', () => { it('should create an auto-follow pattern when cluster is known', async () => { - const name = getRandomString(); - const { body } = await createAutoFollowPattern(name).expect(200); - console.log(body); - + const { body, statusCode } = await createAutoFollowPattern({ + id: 'pattern1', + remoteCluster: REMOTE_CLUSTER_NAME, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); + + expect(statusCode).to.be(200); expect(body.acknowledged).to.eql(true); }); }); describe('get()', () => { it('should return a 404 when the auto-follow pattern is not found', async () => { - const name = getRandomString(); - const { body } = await getAutoFollowPattern(name).expect(404); - + const { body } = await getAutoFollowPattern('missing-pattern'); + expect(body.statusCode).to.be(404); expect(body.attributes.cause).not.to.be(undefined); }); it('should return an auto-follow pattern that was created', async () => { - const name = getRandomString(); - const autoFollowPattern = getAutoFollowIndexPayload(); - - await createAutoFollowPattern(name, autoFollowPattern); - - const { body } = await getAutoFollowPattern(name).expect(200); - - expect(body).to.eql({ ...autoFollowPattern, name }); + await createAutoFollowPattern({ + id: 'pattern2', + remoteCluster: REMOTE_CLUSTER_NAME, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); + + const { body, statusCode } = await getAutoFollowPattern('pattern2'); + + expect(statusCode).to.be(200); + expect(body).to.eql({ + name: 'pattern2', + remoteCluster: REMOTE_CLUSTER_NAME, + active: true, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js index de47f5d9ea85e..6e254b27356f2 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js @@ -7,13 +7,6 @@ import { REMOTE_CLUSTER_NAME } from './constants'; import { getRandomString } from './lib'; -export const getAutoFollowIndexPayload = (remoteCluster = REMOTE_CLUSTER_NAME, active = true) => ({ - active, - remoteCluster, - leaderIndexPatterns: ['leader-*'], - followIndexPattern: '{{leader_index}}_follower', -}); - export const getFollowerIndexPayload = ( leaderIndexName = getRandomString(), remoteCluster = REMOTE_CLUSTER_NAME, diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js index eabf474120f2b..d03b1f83fb404 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../legacy/plugins/cross_cluster_replication/common/constants'; +import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants'; import { getFollowerIndexPayload } from './fixtures'; import { registerHelpers as registerElasticSearchHelpers, getRandomString } from './lib'; import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers'; @@ -57,7 +57,8 @@ export default function({ getService }) { expect(body.attributes.cause[0]).to.contain('no such index'); }); - it('should create a follower index that follows an existing remote index', async () => { + // NOTE: If this test fails locally it's probably because you have another cluster running. + it('should create a follower index that follows an existing leader index', async () => { // First let's create an index to follow const leaderIndex = await createIndex(); @@ -65,7 +66,7 @@ export default function({ getService }) { const { body } = await createFollowerIndex(undefined, payload).expect(200); // There is a race condition in which Elasticsearch can respond without acknowledging, - // i.e. `body .follow_index_shards_acked` is sometimes true and sometimes false. + // i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false. // By only asserting that `follow_index_created` is true, we eliminate this flakiness. expect(body.follow_index_created).to.eql(true); }); @@ -79,6 +80,7 @@ export default function({ getService }) { expect(body.attributes.cause[0]).to.contain('no such index'); }); + // NOTE: If this test fails locally it's probably because you have another cluster running. it('should return a follower index that was created', async () => { const leaderIndex = await createIndex(); diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 7195b8680a286..d2d07eca475e7 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -193,10 +193,10 @@ export default function({ getService }) { 'size', 'isFrozen', 'aliases', - 'ilm', // data enricher - 'isRollupIndex', // data enricher // Cloud disables CCR, so wouldn't expect follower indices. 'isFollowerIndex', // data enricher + 'ilm', // data enricher + 'isRollupIndex', // data enricher ]; expect(Object.keys(body[0])).to.eql(expectedKeys); }); @@ -219,10 +219,10 @@ export default function({ getService }) { 'size', 'isFrozen', 'aliases', - 'ilm', // data enricher - 'isRollupIndex', // data enricher // Cloud disables CCR, so wouldn't expect follower indices. 'isFollowerIndex', // data enricher + 'ilm', // data enricher + 'isRollupIndex', // data enricher ]; expect(Object.keys(body[0])).to.eql(expectedKeys); expect(body.length > 1).to.be(true); // to contrast it with the next test diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts new file mode 100644 index 0000000000000..bbc766df34dcf --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -0,0 +1,145 @@ +/* + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `fq_single_${Date.now()}`; + + const testDataList = [ + { + testTitle: 'ML Poweruser creates a single metric job', + user: USER.ML_POWERUSER, + jobId: `${jobId}_1`, + requestBody: { + job_id: `${jobId}_1`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 200, + responseBody: { + job_id: `${jobId}_1`, + job_type: 'anomaly_detector', + groups: ['automated', 'farequote', 'single-metric'], + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + analysis_config: { + bucket_span: '30m', + summary_count_field_name: 'doc_count', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + analysis_limits: { model_memory_limit: '11mb', categorization_examples_limit: 4 }, + data_description: { time_field: '@timestamp', time_format: 'epoch_ms' }, + model_plot_config: { enabled: true }, + model_snapshot_retention_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + }, + }, + { + testTitle: 'ML viewer cannot create a job', + user: USER.ML_VIEWER, + jobId: `${jobId}_2`, + requestBody: { + job_id: `${jobId}_2`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + message: + '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]', + }, + }, + }, + ]; + + describe('create', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .put(`/api/ml/anomaly_detectors/${testData.jobId}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + expect(body.job_id).to.eql(expectedResponse.job_id); + expect(body.groups).to.eql(expectedResponse.groups); + expect(body.analysis_config!.bucket_span).to.eql( + expectedResponse.analysis_config!.bucket_span + ); + expect(body.analysis_config.detectors).to.have.length( + expectedResponse.analysis_config!.detectors.length + ); + expect(body.analysis_config.detectors[0]).to.eql( + expectedResponse.analysis_config!.detectors[0] + ); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts new file mode 100644 index 0000000000000..fb8acaf5c3ae9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('anomaly detectors', function() { + loadTestFile(require.resolve('./create')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts new file mode 100644 index 0000000000000..dfa81b5d78c65 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts @@ -0,0 +1,248 @@ +/* + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const metricFieldsTestData = { + testTitle: 'returns stats for metric fields over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'JZA' }, // Only use one airline to ensure no sampling. + }, + }, + }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + interval: '1d', + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { + documentCounts: { + interval: '1d', + buckets: { + '1454803200000': 846, + '1454889600000': 846, + '1454976000000': 859, + '1455062400000': 851, + '1455148800000': 858, + }, + }, + }, + { + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + fieldName: 'responsetime', + count: 4260, + min: 963.4293212890625, + max: 1042.13525390625, + avg: 1000.0378077547315, + isTopValuesSampled: false, + topValues: [ + { key: 980.0411987304688, doc_count: 2 }, + { key: 989.278076171875, doc_count: 2 }, + { key: 989.763916015625, doc_count: 2 }, + { key: 991.290771484375, doc_count: 2 }, + { key: 992.0765991210938, doc_count: 2 }, + { key: 993.8115844726562, doc_count: 2 }, + { key: 993.8973999023438, doc_count: 2 }, + { key: 994.0230102539062, doc_count: 2 }, + { key: 994.364990234375, doc_count: 2 }, + { key: 994.916015625, doc_count: 2 }, + ], + topValuesSampleSize: 4260, + topValuesSamplerShardSize: -1, + }, + ], + }, + }; + + const nonMetricFieldsTestData = { + testTitle: 'returns stats for non-metric fields specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + fields: [ + { fieldName: '@timestamp', type: 'date', cardinality: 4751 }, + { fieldName: '@version.keyword', type: 'keyword', cardinality: 1 }, + { fieldName: 'airline', type: 'keyword', cardinality: 19 }, + { fieldName: 'type', type: 'text', cardinality: 0 }, + { fieldName: 'type.keyword', type: 'keyword', cardinality: 1 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { fieldName: '@timestamp', count: 1733, earliest: 1454889602000, latest: 1454975948000 }, + { + fieldName: '@version.keyword', + isTopValuesSampled: false, + topValues: [{ key: '1', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'airline', + isTopValuesSampled: false, + topValues: [{ key: 'AAL', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'type.keyword', + isTopValuesSampled: false, + topValues: [{ key: 'farequote', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { fieldName: 'type', examples: ['farequote'] }, + ], + }, + }; + + const errorTestData = { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exists', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + maxExamples: 10, + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + }, + }, + }; + + async function runGetFieldStatsRequest( + index: string, + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_field_stats/${index}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareByFieldName(a: { fieldName: string }, b: { fieldName: string }) { + if (a.fieldName < b.fieldName) { + return -1; + } + if (a.fieldName > b.fieldName) { + return 1; + } + return 0; + } + + describe('get_field_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + it(`${metricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + metricFieldsTestData.index, + metricFieldsTestData.user, + metricFieldsTestData.requestBody, + metricFieldsTestData.expected.responseCode + ); + + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + const expected = metricFieldsTestData.expected; + expect(body).to.have.length(expected.responseBody.length); + + const actualDocCounts = body[0]; + const expectedDocCounts = expected.responseBody[0]; + expect(actualDocCounts).to.eql(expectedDocCounts); + + const actualFieldData = { ...body[1] }; + delete actualFieldData.median; + delete actualFieldData.distribution; + + expect(actualFieldData).to.eql(expected.responseBody[1]); + }); + + it(`${nonMetricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + nonMetricFieldsTestData.index, + nonMetricFieldsTestData.user, + nonMetricFieldsTestData.requestBody, + nonMetricFieldsTestData.expected.responseCode + ); + + // Sort the fields in the response before validating. + const expectedRspFields = nonMetricFieldsTestData.expected.responseBody.sort( + compareByFieldName + ); + const actualRspFields = body.sort(compareByFieldName); + expect(actualRspFields).to.eql(expectedRspFields); + }); + + it(`${errorTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + errorTestData.index, + errorTestData.user, + errorTestData.requestBody, + errorTestData.expected.responseCode + ); + + expect(body.error).to.eql(errorTestData.expected.responseBody.error); + expect(body.message).to.eql(errorTestData.expected.responseBody.message); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts new file mode 100644 index 0000000000000..6490c19c64483 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts @@ -0,0 +1,154 @@ +/* + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns stats over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 86274, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 78580 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 19 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 83346 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns stats when specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 1733, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1713 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1730 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exist', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['@version', 'type'], + samplerShardSize: 1000, + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exist], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exist" & index_uuid="_na_" & index="ft_farequote_not_exist" }', + }, + }, + }, + ]; + + describe('get_overall_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_overall_stats/${testData.index}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts new file mode 100644 index 0000000000000..ce9e44618f1af --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('data visualizer', function() { + loadTestFile(require.resolve('./get_field_stats')); + loadTestFile(require.resolve('./get_overall_stats')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts new file mode 100644 index 0000000000000..245375562b5c1 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts @@ -0,0 +1,115 @@ +/* + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns cardinality of customer name fields over full time range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + 'customer_first_name.keyword': 46, + 'customer_last_name.keyword': 183, + }, + }, + }, + { + testTitle: 'returns cardinality of geoip fields over specified range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['geoip.city_name', 'geoip.continent_name', 'geoip.country_iso_code'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: { + 'geoip.city_name': 10, + 'geoip.continent_name': 5, + 'geoip.country_iso_code': 9, + }, + }, + }, + { + testTitle: 'returns empty response for non aggregatable field', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['manufacturer'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: {}, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('field_cardinality', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/field_cardinality') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/fields_service/index.ts b/x-pack/test/api_integration/apis/ml/fields_service/index.ts new file mode 100644 index 0000000000000..312602e589119 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('fields service', function() { + loadTestFile(require.resolve('./field_cardinality')); + loadTestFile(require.resolve('./time_field_range')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts new file mode 100644 index 0000000000000..2f0fd4fc6c5e3 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts @@ -0,0 +1,119 @@ +/* + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns expected time range with index and match_all query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560297859000, + string: '2019-06-12T00:04:19.000Z', + }, + end: { + epoch: 1562975136000, + string: '2019-07-12T23:45:36.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns expected time range with index and query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { + term: { + 'customer_first_name.keyword': { + value: 'Brigitte', + }, + }, + }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560298982000, + string: '2019-06-12T00:23:02.000Z', + }, + end: { + epoch: 1562973754000, + string: '2019-07-12T23:22:34.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('time_field_range', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/time_field_range') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index f012883c46ca3..58356637c63ac 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -31,11 +31,11 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./bucket_span_estimator')); - loadTestFile(require.resolve('./calculate_model_memory_limit')); - loadTestFile(require.resolve('./categorization_field_examples')); - loadTestFile(require.resolve('./get_module')); - loadTestFile(require.resolve('./recognize_module')); - loadTestFile(require.resolve('./setup_module')); + loadTestFile(require.resolve('./modules')); + loadTestFile(require.resolve('./anomaly_detectors')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./fields_service')); + loadTestFile(require.resolve('./job_validation')); + loadTestFile(require.resolve('./jobs')); }); } diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts rename to x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts index bc0dc3019d7c9..0b4aca9660be4 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts rename to x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts index 59e3dfcca00f9..f17814633ce8f 100644 --- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts new file mode 100644 index 0000000000000..6ca9dcbbe9e5b --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('job validation', function() { + loadTestFile(require.resolve('./bucket_span_estimator')); + loadTestFile(require.resolve('./calculate_model_memory_limit')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts similarity index 98% rename from x-pack/test/api_integration/apis/ml/categorization_field_examples.ts rename to x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts index df0153f965942..bcc6c4907100c 100644 --- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts new file mode 100644 index 0000000000000..70a64f198d6f4 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('jobs', function() { + loadTestFile(require.resolve('./categorization_field_examples')); + loadTestFile(require.resolve('./jobs_summary')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts new file mode 100644 index 0000000000000..6a57db1687868 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -0,0 +1,374 @@ +/* + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; +import { Job } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const SINGLE_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_single_${Date.now()}`, + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, +}; + +const MULTI_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_multi_${Date.now()}`, + description: 'mean(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: true }, +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + const testDataListNoJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + { + testTitle: 'as ML Viewer', + user: USER.ML_VIEWER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListWithJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: { + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id], + }, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + fullJob: { + // Only tests against some of the fields in the fullJob property. + job_id: SINGLE_METRIC_JOB_CONFIG.job_id, + job_type: 'anomaly_detector', + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + }, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListNegative = [ + { + testTitle: 'as ML Unauthorized user', + user: USER.ML_UNAUTHORIZED, + requestBody: {}, + // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. + expected: { + responseCode: 403, + error: 'Forbidden', + }, + }, + ]; + + async function runJobsSummaryRequest( + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise { + const { body } = await supertest + .post('/api/ml/jobs/jobs_summary') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareById(a: { id: string }, b: { id: string }) { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + } + + function getGroups(jobs: Array<{ groups: string[] }>) { + const groupIds: string[] = []; + jobs.forEach(job => { + const groups = job.groups; + groups.forEach(group => { + if (groupIds.indexOf(group) === -1) { + groupIds.push(group); + } + }); + }); + return groupIds.sort(); + } + + describe('jobs_summary', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('sets up jobs', async () => { + for (const job of testSetupJobConfigs) { + await ml.api.createAnomalyDetectionJob(job); + } + }); + + for (const testData of testDataListNoJobId) { + describe('gets job summary with no job IDs supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + }); + }); + } + + for (const testData of testDataListWithJobId) { + describe('gets job summary with job ID supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + + // Validate the response for the specified job IDs contains a fullJob property. + const requestedJobIds = testData.requestBody.jobIds; + for (const job of body) { + if (requestedJobIds.includes(job.id)) { + expect(job).to.have.property('fullJob'); + } else { + expect(job).not.to.have.property('fullJob'); + } + } + + for (const expectedJob of expectedResponse) { + const expectedJobId = expectedJob.id; + const actualJob = body.find((job: { id: string }) => job.id === expectedJobId); + if (expectedJob.fullJob) { + expect(actualJob).to.have.property('fullJob'); + expect(actualJob.fullJob).to.have.property('analysis_config'); + expect(actualJob.fullJob.analysis_config).to.eql(expectedJob.fullJob.analysis_config); + } else { + expect(actualJob).not.to.have.property('fullJob'); + } + } + }); + }); + } + + for (const testData of testDataListNegative) { + describe('rejects request', function() { + it(testData.testTitle, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(body) + .to.have.property('error') + .eql(testData.expected.error); + + expect(body).to.have.property('message'); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts similarity index 92% rename from x-pack/test/api_integration/apis/ml/get_module.ts rename to x-pack/test/api_integration/apis/ml/modules/get_module.ts index a50d3c0abe430..e19d45999c88e 100644 --- a/x-pack/test/api_integration/apis/ml/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/modules/index.ts b/x-pack/test/api_integration/apis/ml/modules/index.ts new file mode 100644 index 0000000000000..4fdc404c607aa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/modules/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('modules', function() { + loadTestFile(require.resolve('./get_module')); + loadTestFile(require.resolve('./recognize_module')); + loadTestFile(require.resolve('./setup_module')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts similarity index 93% rename from x-pack/test/api_integration/apis/ml/recognize_module.ts rename to x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 8e360579c1459..948728189b8bd 100644 --- a/x-pack/test/api_integration/apis/ml/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts similarity index 96% rename from x-pack/test/api_integration/apis/ml/setup_module.ts rename to x-pack/test/api_integration/apis/ml/modules/setup_module.ts index e603782b25717..23ddd3b63a2ef 100644 --- a/x-pack/test/api_integration/apis/ml/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -6,10 +6,10 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/security/api_keys.ts b/x-pack/test/api_integration/apis/security/api_keys.ts new file mode 100644 index 0000000000000..276a5367a419e --- /dev/null +++ b/x-pack/test/api_integration/apis/security/api_keys.ts @@ -0,0 +1,28 @@ +/* + * 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/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('API Keys', () => { + describe('GET /internal/security/api_key/_enabled', () => { + it('should indicate that API Keys are enabled', async () => { + await supertest + .get('/internal/security/api_key/_enabled') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200) + .then((response: Record) => { + const payload = response.body; + expect(payload).to.eql({ apiKeysEnabled: true }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index ad1876cb717f1..7bb79a589d522 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -11,6 +11,7 @@ export default function({ loadTestFile }) { // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index dcbdb17724249..3e426f210afa8 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -13,6 +13,7 @@ export default function({ loadTestFile }: FtrProviderContext) { // Updates here should be mirrored in `./index.js` if tests // should also run under a trial/platinum license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index a1b731169f0a0..ea980721b831b 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -5,8 +5,10 @@ */ import expect from '@kbn/expect'; +import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { defaultDynamicSettings } from '../../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { DynamicSettingsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -14,15 +16,16 @@ export default function({ getService }: FtrProviderContext) { describe('dynamic settings', () => { it('returns the defaults when no user settings have been saved', async () => { const apiResponse = await supertest.get(`/api/uptime/dynamic_settings`); - expect(apiResponse.body).to.eql(defaultDynamicSettings as any); + expect(apiResponse.body).to.eql(DYNAMIC_SETTINGS_DEFAULTS); + expect(isRight(DynamicSettingsType.decode(apiResponse.body))).to.be.ok(); }); it('can change the settings', async () => { const newSettings = { heartbeatIndices: 'myIndex1*', - certificatesThresholds: { - errorState: 5, - warningState: 15, + certThresholds: { + expiration: 5, + age: 15, }, }; const postResponse = await supertest @@ -35,6 +38,7 @@ export default function({ getService }: FtrProviderContext) { const getResponse = await supertest.get(`/api/uptime/dynamic_settings`); expect(getResponse.body).to.eql(newSettings); + expect(isRight(DynamicSettingsType.decode(getResponse.body))).to.be.ok(); }); }); } diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 0eac7c58044e6..dda8c2d888d30 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -30,7 +30,6 @@ export async function getApiIntegrationConfig({ readConfigFile }) { '--telemetry.optIn=true', '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', - '--xpack.ingestManager.fleet.enabled=true', '--xpack.endpoint.alertResultListDefaultDateRange.from=2018-01-10T00:00:00.000Z', ], }, diff --git a/x-pack/test/api_integration/config_security_basic.js b/x-pack/test/api_integration/config_security_basic.js index d21bfa4d7031a..4c4b77ee5b080 100644 --- a/x-pack/test/api_integration/config_security_basic.js +++ b/x-pack/test/api_integration/config_security_basic.js @@ -13,6 +13,7 @@ export default async function({ readConfigFile }) { config.esTestCluster.serverArgs = [ 'xpack.license.self_generated.type=basic', 'xpack.security.enabled=true', + 'xpack.security.authc.api_key.enabled=true', ]; config.testFiles = [require.resolve('./apis/security/security_basic')]; return config; diff --git a/x-pack/test/case_api_integration/basic/config.ts b/x-pack/test/case_api_integration/basic/config.ts new file mode 100644 index 0000000000000..f9c248ec3d56f --- /dev/null +++ b/x-pack/test/case_api_integration/basic/config.ts @@ -0,0 +1,14 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('basic', { + disabledPlugins: [], + license: 'basic', + ssl: true, +}); diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts new file mode 100644 index 0000000000000..a9fc2706a6ba2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts @@ -0,0 +1,55 @@ +/* + * 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 '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return an empty find body correctly if no configuration is loaded', async () => { + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({}); + }); + + it('should return a configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts new file mode 100644 index 0000000000000..836c76d500034 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../plugins/case/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('get_connectors', () => { + it('should return an empty find body correctly if no connectors are loaded', async () => { + const { body } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts new file mode 100644 index 0000000000000..d66baa2a2eee2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts @@ -0,0 +1,81 @@ +/* + * 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 '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should patch a configuration', async () => { + const res = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: res.body.version }) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); + }); + + it('should handle patch request when there is no configuration', async () => { + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'You can not patch this configuration since you did not created first with a post.', + statusCode: 409, + }); + }); + + it('should handle patch request when versions are different', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'This configuration has been updated. Please refresh before saving additional updates.', + statusCode: 409, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts new file mode 100644 index 0000000000000..c2284492e5b77 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts @@ -0,0 +1,62 @@ +/* + * 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 '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should create a configuration', async () => { + const { body } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should keep only the latest configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration('connector-2')) + .expect(200); + + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts new file mode 100644 index 0000000000000..efd5369c019d8 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('case api basic', function() { + // Fastest ciGroup for the moment. + this.tags('ciGroup2'); + + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/get_connectors')); + }); +}; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts new file mode 100644 index 0000000000000..862705ab9610b --- /dev/null +++ b/x-pack/test/case_api_integration/common/config.ts @@ -0,0 +1,94 @@ +/* + * 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 path from 'path'; + +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.server-log', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers, + services, + junit: { + reportName: 'X-Pack Case API Integration Tests', + }, + esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'), + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && + ['trial', 'basic'].includes(license)}`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.whitelistedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + ])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.alerting.enabled=true', + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/case_api_integration/common/ftr_provider_context.d.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. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts new file mode 100644 index 0000000000000..6d0db69309b90 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -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 { CasesConfigureRequest, CasesConfigureResponse } from '../../../../plugins/case/common/api'; + +export const getConfiguration = (connector_id: string = 'connector-1'): CasesConfigureRequest => { + return { + connector_id, + connector_name: 'Connector 1', + closure_type: 'close-by-user', + }; +}; + +export const getConfigurationOutput = (update = false): Partial => { + return { + ...getConfiguration(), + created_by: { email: null, full_name: null, username: 'elastic' }, + updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null, + }; +}; + +export const removeServerGeneratedPropertiesFromConfigure = ( + config: Partial +): Partial => { + const { created_at, updated_at, version, ...rest } = config; + return rest; +}; + +export const deleteConfiguration = async (es: any): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:cases-configure', + waitForCompletion: true, + refresh: 'wait_for', + body: {}, + }); +}; + +export const getConnector = () => ({ + name: 'ServiceNow Connector', + actionTypeId: '.servicenow', + secrets: { + username: 'admin', + password: 'admin', + }, + config: { + apiUrl: 'localhost', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, +}); diff --git a/x-pack/test/case_api_integration/common/services.ts b/x-pack/test/case_api_integration/common/services.ts new file mode 100644 index 0000000000000..a927a31469bab --- /dev/null +++ b/x-pack/test/case_api_integration/common/services.ts @@ -0,0 +1,7 @@ +/* + * 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 { services } from '../../api_integration/services'; diff --git a/x-pack/test/epm_api_integration/config.ts b/x-pack/test/epm_api_integration/config.ts index e95a389ef20ed..b04bc76ccb315 100644 --- a/x-pack/test/epm_api_integration/config.ts +++ b/x-pack/test/epm_api_integration/config.ts @@ -28,7 +28,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.ingestManager.epm.enabled=true', '--xpack.ingestManager.epm.registryUrl=http://localhost:6666', ], }, diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index de3976509be1f..d4e1702368879 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,8 +19,7 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - // FLAKY: https://github.com/elastic/kibana/issues/62927 - describe.skip('custom elements', function() { + describe('custom elements', function() { this.tags('skipFirefox'); before(async () => { @@ -42,7 +41,7 @@ export default function canvasCustomElementTest({ await testSubjects.click('canvasWorkpadPage > canvasWorkpadPageElementContent', 20000); // click the "Save as new element" button - await find.clickByCssSelector('[aria-label="Save as new element"]', 20000); + await testSubjects.click('canvasSidebarHeader__saveElementButton', 20000); // fill out the custom element form and submit it await PageObjects.canvas.fillOutCustomElementForm( @@ -57,15 +56,11 @@ export default function canvasCustomElementTest({ }); it('adds the custom element to the workpad when prompted', async () => { - await PageObjects.canvas.openAddElementModal(); - - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the custom element is the one expected and click it to add to the workpad - const customElement = await find.byCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper' - ); + const customElement = await find.byCssSelector('.canvasElementCard__wrapper'); const elementName = await customElement.findByCssSelector('.euiCard__title'); expect(await elementName.getVisibleText()).to.contain('My New Element'); customElement.click(); @@ -95,14 +90,11 @@ export default function canvasCustomElementTest({ }); it('saves custom element modifications', async () => { - await PageObjects.canvas.openAddElementModal(); - - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the correct amount of custom elements exist - const container = await find.byCssSelector('[aria-labelledby="customElements"]'); - const customElements = await container.findAllByCssSelector('.canvasElementCard__wrapper'); + const customElements = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElements).to.have.length(1); // hover over the custom element to bring up the edit and delete icons @@ -110,8 +102,7 @@ export default function canvasCustomElementTest({ await customElement.moveMouseTo(); // click the edit element button - const editBtn = await customElement.findByCssSelector('[aria-label="Edit element"]'); - await editBtn.click(); + await testSubjects.click('canvasElementCard__editButton', 20000); // fill out the custom element form and submit it await PageObjects.canvas.fillOutCustomElementForm( @@ -121,22 +112,21 @@ export default function canvasCustomElementTest({ // ensure the custom element in the modal shows the updated text await retry.try(async () => { - const elementName = await find.byCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper .euiCard__title' - ); + const elementName = await find.byCssSelector('.canvasElementCard__wrapper .euiCard__title'); expect(await elementName.getVisibleText()).to.contain('My Edited New Element'); }); + + // Close the modal + await PageObjects.canvas.closeSavedElementsModal(); }); it('deletes custom element when prompted', async () => { - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the correct amount of custom elements exist - const customElements = await find.allByCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper' - ); + const customElements = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElements).to.have.length(1); // hover over the custom element to bring up the edit and delete icons @@ -144,22 +134,18 @@ export default function canvasCustomElementTest({ await customElement.moveMouseTo(); // click the delete element button - const editBtn = await customElement.findByCssSelector('[aria-label="Delete element"]'); - await editBtn.click(); + await testSubjects.click('canvasElementCard__deleteButton', 20000); - await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton', 20000); // ensure the custom element was deleted await retry.try(async () => { - const containerAgain = await find.byCssSelector('[aria-labelledby="customElements"]'); - const customElementsAgain = await containerAgain.findAllByCssSelector( - '.canvasElementCard__wrapper' - ); + const customElementsAgain = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElementsAgain).to.have.length(0); }); // Close the modal - await browser.pressKeys(browser.keys.ESCAPE); + await PageObjects.canvas.closeSavedElementsModal(); }); }); } diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index dc8c488460100..76ca613af4b55 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -28,8 +28,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY: https://github.com/elastic/kibana/issues/60535 - describe.skip('security', () => { + describe('security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index f33b8b4899d16..4bedc757f0b57 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -24,8 +24,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY: https://github.com/elastic/kibana/issues/60559 - describe.skip('spaces', () => { + describe('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); diff --git a/x-pack/test/functional/apps/reporting_management/index.ts b/x-pack/test/functional/apps/reporting_management/index.ts new file mode 100644 index 0000000000000..07306d8fb2956 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('reporting management app', function() { + this.tags('ciGroup7'); + loadTestFile(require.resolve('./report_delete_pagination')); + }); +}; diff --git a/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts b/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts new file mode 100644 index 0000000000000..5b2edad9e6d95 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts @@ -0,0 +1,59 @@ +/* + * 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'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'reporting']); + const log = getService('log'); + const retry = getService('retry'); + const security = getService('security'); + + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + + describe('Delete reports', function() { + before(async () => { + await security.testUser.setRoles(['global_discover_read', 'reporting_user']); + await esArchiver.load('empty_kibana'); + await esArchiver.load('reporting/archived_reports'); + await pageObjects.common.navigateToActualUrl('kibana', '/management/kibana/reporting'); + await testSubjects.existOrFail('reportJobListing', { timeout: 200000 }); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await esArchiver.unload('reporting/archived_reports'); + await security.testUser.restoreDefaults(); + }); + + it('Confirm single report deletion works', async () => { + log.debug('Checking for reports.'); + await retry.try(async () => { + await testSubjects.click('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + }); + const deleteButton = await testSubjects.find('deleteReportButton'); + await retry.waitFor('delete button to become enabled', async () => { + return await deleteButton.isEnabled(); + }); + await deleteButton.click(); + await testSubjects.exists('confirmModalBodyText'); + await testSubjects.click('confirmModalConfirmButton'); + await retry.try(async () => { + await testSubjects.waitForDeleted('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + }); + }); + + // functional test for report pagination: https://github.com/elastic/kibana/pull/62881 + it('Report pagination', async () => { + const previousButton = await testSubjects.find('pagination-button-previous'); + expect(await previousButton.getAttribute('disabled')).to.be('true'); + await testSubjects.click('pagination-button-1'); + expect(await previousButton.getAttribute('disabled')).to.be(null); + }); + }); +}; diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 8ab84126b2b30..3bcf504c17a6f 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -19,7 +19,8 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'header']); - describe('Management', function() { + // FLAKY: https://github.com/elastic/kibana/issues/61173 + describe.skip('Management', function() { this.tags(['skipFirefox']); before(async () => { diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index bea6b814ee8a3..dbdbca0368146 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -90,7 +90,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -144,7 +144,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -180,14 +180,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the source index preview', async () => { - await transform.wizard.assertSourceIndexPreview( - testData.expected.sourcePreview.columns, - testData.expected.sourcePreview.rows + it('shows the index preview', async () => { + await transform.wizard.assertIndexPreview( + testData.expected.indexPreview.columns, + testData.expected.indexPreview.rows ); }); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 993bd3a79abbc..fd5673de0d7a7 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -65,7 +65,7 @@ export default function({ getService }: FtrProviderContext) { progress: '100', }, sourceIndex: 'ft_farequote', - sourcePreview: { + indexPreview: { column: 2, values: ['ASA'], }, @@ -101,14 +101,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the filtered source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewColumnValues( - testData.expected.sourcePreview.column, - testData.expected.sourcePreview.values + it('shows the filtered index preview', async () => { + await transform.wizard.assertIndexPreviewColumnValues( + testData.expected.indexPreview.column, + testData.expected.indexPreview.values ); }); diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index e81bbc5ae42f9..64b6300e0df63 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -6,10 +6,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { - defaultDynamicSettings, - DynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; +import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; import { makeChecks } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; export default ({ getPageObjects, getService }: FtrProviderContext) => { @@ -32,7 +30,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await settings.go(); const fields = await settings.loadFields(); - expect(fields).to.eql(defaultDynamicSettings); + expect(fields).to.eql(DYNAMIC_SETTINGS_DEFAULTS); }); it('should disable the apply button when invalid or unchanged', async () => { @@ -62,7 +60,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await settings.go(); - const newFieldValues: DynamicSettings = { heartbeatIndices: 'new*' }; + const newFieldValues: DynamicSettings = { + heartbeatIndices: 'new*', + certThresholds: { + age: 365, + expiration: 30, + }, + }; await settings.changeHeartbeatIndicesInput(newFieldValues.heartbeatIndices); await settings.apply(); @@ -91,7 +95,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields.certificatesThresholds.errorState).to.eql(newErrorThreshold); + expect(fields.certThresholds?.expiration).to.eql(newErrorThreshold); }); it('changing certificate expiration warning threshold is reflected in settings page', async () => { @@ -108,7 +112,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields.certificatesThresholds.warningState).to.eql(newWarningThreshold); + expect(fields.certThresholds?.age).to.eql(newWarningThreshold); }); }); }; diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index f26110513a9b3..2c6238704bea0 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -57,6 +57,8 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), resolve(__dirname, './apps/transform'), + resolve(__dirname, './apps/reporting_management'), + // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), ], @@ -196,6 +198,10 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana/', hash: '/management/elasticsearch/transform', }, + reporting: { + pathname: '/app/kibana/', + hash: '/management/kibana/reporting', + }, }, // choose where esArchiver should load archives from @@ -228,6 +234,17 @@ export default async function({ readConfigFile }) { kibana: [], }, + global_discover_read: { + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }, + //Kibana feature privilege isn't specific to advancedSetting. It can be anything. https://github.com/elastic/kibana/issues/35965 test_api_keys: { elasticsearch: { diff --git a/x-pack/test/functional/es_archives/empty_kibana/data.json.gz b/x-pack/test/functional/es_archives/empty_kibana/data.json.gz deleted file mode 100644 index 8334749a696d7..0000000000000 Binary files a/x-pack/test/functional/es_archives/empty_kibana/data.json.gz and /dev/null differ diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz index a71281c0ecfec..3d4f0e11a7cc6 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz new file mode 100644 index 0000000000000..34a30bd84a592 Binary files /dev/null and b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json new file mode 100644 index 0000000000000..20f8f840ee863 --- /dev/null +++ b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json @@ -0,0 +1,108 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": ".reporting-2020.04.19", + "mappings": { + "properties": { + "attempts": { + "type": "short" + }, + "browser_type": { + "type": "keyword" + }, + "completed_at": { + "type": "date" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "jobtype": { + "type": "keyword" + }, + "kibana_id": { + "type": "keyword" + }, + "kibana_name": { + "type": "keyword" + }, + "max_attempts": { + "type": "short" + }, + "meta": { + "properties": { + "layout": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "objectType": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "output": { + "properties": { + "content": { + "enabled": false, + "type": "object" + }, + "content_type": { + "type": "keyword" + }, + "csv_contains_formulas": { + "type": "boolean" + }, + "max_size_reached": { + "type": "boolean" + }, + "size": { + "type": "long" + } + } + }, + "payload": { + "enabled": false, + "type": "object" + }, + "priority": { + "type": "byte" + }, + "process_expiration": { + "type": "date" + }, + "started_at": { + "type": "date" + }, + "status": { + "type": "keyword" + }, + "timeout": { + "type": "long" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "prefer_v2_templates": "false" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index de826097a5be6..94ad393ead3a3 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -51,8 +51,12 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { expect(disabledAttr).to.be('true'); }, - async openAddElementModal() { + async openSavedElementsModal() { await testSubjects.click('add-element-button'); + await testSubjects.click('saved-elements-menu-option'); + }, + async closeSavedElementsModal() { + await testSubjects.click('saved-elements-modal-close-button'); }, async expectAddElementButton() { diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js index cdfafeec1bf46..35a76c74d4811 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.js @@ -170,6 +170,5 @@ export function ReportingPageProvider({ getService, getPageObjects }) { await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); } } - return new ReportingPage(); } diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index b399327012a77..84eb0cc378771 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -385,7 +385,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { // have to remove the '*' return find .clickByCssSelector( - 'div[data-test-subj="fieldInput0"] .euiBadge[title="*"] svg.euiIcon' + 'div[data-test-subj="fieldInput0"] [title="Remove * from selection in this group"] svg.euiIcon' ) .then(function() { return addGrantedField(userObj.elasticsearch.indices[0].field_security.grant); diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts index bada1d42b564a..bd7d76e34b447 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts @@ -41,7 +41,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertRegressionTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertClassificationEvaluatePanelElementsExists() { @@ -50,7 +50,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertClassificationTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertOutlierTablePanelExists() { diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index ba9096a372d9a..e63af493438d6 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -52,8 +52,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertDetailsSummaryExists(); }, - async assertSourceIndexPreviewExists(subSelector?: string) { - let selector = 'transformSourceIndexPreview'; + async assertIndexPreviewExists(subSelector?: string) { + let selector = 'transformIndexPreview'; if (subSelector !== undefined) { selector = `${selector} ${subSelector}`; } else { @@ -62,8 +62,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, - async assertSourceIndexPreviewLoaded() { - await this.assertSourceIndexPreviewExists('loaded'); + async assertIndexPreviewLoaded() { + await this.assertIndexPreviewExists('loaded'); }, async assertPivotPreviewExists(subSelector?: string) { @@ -124,10 +124,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreview(columns: number, rows: number) { + async assertIndexPreview(columns: number, rows: number) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rowsData = await this.parseEuiDataGrid('transformSourceIndexPreview'); + const rowsData = await this.parseEuiDataGrid('transformIndexPreview'); expect(rowsData).to.length( rows, @@ -143,8 +143,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { - await this.assertEuiDataGridColumnValues('transformSourceIndexPreview', column, values); + async assertIndexPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiDataGridColumnValues('transformIndexPreview', column, values); }, async assertPivotPreviewColumnValues(column: number, values: string[]) { diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index 14cab368b766a..9719152b62d35 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -5,6 +5,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; +import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); @@ -25,24 +26,24 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) { await changeInputField(text, 'heartbeat-indices-input-loaded'); }, changeErrorThresholdInput: async (text: string) => { - await changeInputField(text, 'error-state-threshold-input-loaded'); + await changeInputField(text, 'expiration-threshold-input-loaded'); }, changeWarningThresholdInput: async (text: string) => { - await changeInputField(text, 'warning-state-threshold-input-loaded'); + await changeInputField(text, 'age-threshold-input-loaded'); }, - loadFields: async () => { + loadFields: async (): Promise => { const indInput = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - const errorInput = await testSubjects.find('error-state-threshold-input-loaded', 5000); - const warningInput = await testSubjects.find('warning-state-threshold-input-loaded', 5000); + const expirationInput = await testSubjects.find('expiration-threshold-input-loaded', 5000); + const ageInput = await testSubjects.find('age-threshold-input-loaded', 5000); const heartbeatIndices = await indInput.getAttribute('value'); - const errorThreshold = await errorInput.getAttribute('value'); - const warningThreshold = await warningInput.getAttribute('value'); + const expiration = await expirationInput.getAttribute('value'); + const age = await ageInput.getAttribute('value'); return { heartbeatIndices, - certificatesThresholds: { - errorState: errorThreshold, - warningState: warningThreshold, + certThresholds: { + age: parseInt(age, 10), + expiration: parseInt(expiration, 10), }, }; }, diff --git a/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts index c543046031e9f..fdebdae9e5d0e 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -41,13 +41,13 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('welcomeTitle'); }); - it(`endpoint management shows 'Hosts'`, async () => { + it(`endpoint hosts shows hosts lists page`, async () => { await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts', undefined, { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); }); }); diff --git a/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts index c2c4068212484..b944056e00911 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts @@ -31,7 +31,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the hosts page when the Hosts tab is selected', async () => { await (await testSubjects.find('hostsEndpointTab')).click(); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); }); it('renders the alerts page when the Alerts tab is selected', async () => { @@ -46,7 +46,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the home page when Home tab is selected after selecting another tab', async () => { await (await testSubjects.find('hostsEndpointTab')).click(); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); await (await testSubjects.find('homeEndpointTab')).click(); await testSubjects.existOrFail('welcomeTitle'); diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts index 6ae78ab9d48ac..d7f1cc21828d1 100644 --- a/x-pack/test/functional_endpoint/config.ts +++ b/x-pack/test/functional_endpoint/config.ts @@ -30,8 +30,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', - '--xpack.ingestManager.epm.enabled=true', - '--xpack.ingestManager.fleet.enabled=true', ], }, }; diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 3b11ef61a1ab2..0b127288e7958 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -35,7 +35,7 @@ export default function({ getService }: FtrProviderContext) { }); } - async function checkSessionCookie(sessionCookie: Cookie) { + async function checkSessionCookie(sessionCookie: Cookie, username = 'a@b.c') { expect(sessionCookie.key).to.be('sid'); expect(sessionCookie.value).to.not.be.empty(); expect(sessionCookie.path).to.be('/'); @@ -59,7 +59,7 @@ export default function({ getService }: FtrProviderContext) { 'authentication_provider', ]); - expect(apiResponse.body.username).to.be('a@b.c'); + expect(apiResponse.body.username).to.be(username); } describe('SAML authentication', () => { @@ -668,6 +668,29 @@ export default function({ getService }: FtrProviderContext) { const existingUsername = 'a@b.c'; let existingSessionCookie: Cookie; + const testScenarios: Array<[string, () => Promise]> = [ + // Default scenario when active cookie has an active access token. + ['when access token is valid', async () => {}], + // Scenario when active cookie has an expired access token. Access token expiration is set + // to 15s for API integration tests so we need to wait for 20s to make sure token expires. + ['when access token is expired', async () => await delay(20000)], + // Scenario when active cookie references to access/refresh token pair that were already + // removed from Elasticsearch (to simulate 24h when expired tokens are removed). + [ + 'when access token document is missing', + async () => { + const esResponse = await getService('legacyEs').deleteByQuery({ + index: '.security-tokens', + q: 'doc_type:token', + refresh: true, + }); + expect(esResponse) + .to.have.property('deleted') + .greaterThan(0); + }, + ], + ]; + beforeEach(async () => { const captureURLResponse = await supertest .get('/abc/xyz/handshake?one=two three') @@ -701,76 +724,76 @@ export default function({ getService }: FtrProviderContext) { )!; }); - it('should renew session and redirect to the home page if login is for the same user', async () => { - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) - .expect('location', '/') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); - - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', existingUsername); - }); - - it('should create a new session and redirect to the `overwritten_session` if login is for another user', async () => { - const newUsername = 'c@d.e'; - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) - .expect('location', '/security/overwritten_session') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); + for (const [description, setup] of testScenarios) { + it(`should renew session and redirect to the home page if login is for the same user ${description}`, async () => { + await setup(); + + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be('/'); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie); + }); - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', newUsername); - }); + it(`should create a new session and redirect to the \`overwritten_session\` if login is for another user ${description}`, async () => { + await setup(); + + const newUsername = 'c@d.e'; + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be( + '/security/overwritten_session' + ); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie, newUsername); + }); + } }); describe('handshake with very long URL path or fragment', () => { diff --git a/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz b/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz new file mode 100644 index 0000000000000..c7acb36992af3 Binary files /dev/null and b/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz differ diff --git a/x-pack/test/siem_cypress/es_archives/timeline/mappings.json b/x-pack/test/siem_cypress/es_archives/timeline/mappings.json new file mode 100644 index 0000000000000..d3412f9d43b57 --- /dev/null +++ b/x-pack/test/siem_cypress/es_archives/timeline/mappings.json @@ -0,0 +1,2976 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "agent_actions": "ed270b46812f0fa1439366c428a2cf17", + "agent_configs": "38abaf89513877745c359e7700c0c66a", + "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "agents": "c3eeb7b9d97176f15f6d126370ab23c7", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "08b8b110dbca273d37e8aef131ecab61", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", + "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "epm-package": "0be91c6758421dd5d0f1a58e9e5bc7c3", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "9ecce5b58867403613d82fe496470b34", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "outputs": "aee9782e0d500b867859650a36280165", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "siem-detection-engine-rule-actions": "90eee2e4635260f4be0a1da8f5bc0aa0", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "dynamic": "true", + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b47befbf9057b..b9f80cd5923b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1088,7 +1088,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.4.2", "@babel/runtime-corejs2@^7.6.3": +"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.6.3": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.9.2.tgz#f11d074ff99b9b4319b5ecf0501f12202bf2bf4d" integrity sha512-ayjSOxuK2GaSDJFCtLgHnYjuMyIpViNujWrZo8GUpN60/n7juzJKK5yOo6RFVb0zdU9ACJFK+MsZrUnj3OmXMw== @@ -1096,14 +1096,6 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/runtime@7.0.0-beta.54": - version "7.0.0-beta.54" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf" - integrity sha1-Oeu0JyP+fKSz4bAOln6AE41Hyt8= - dependencies: - core-js "^2.5.7" - regenerator-runtime "^0.12.0" - "@babel/runtime@7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" @@ -1173,7 +1165,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.9.5": +"@babel/types@^7.4", "@babel/types@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== @@ -1244,10 +1236,10 @@ dependencies: "@elastic/apm-rum-core" "^5.2.0" -"@elastic/charts@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.3.0.tgz#cbdeec1860af274edc7a5f5b9dd26ec48c64bb64" - integrity sha512-4kSlSwdDRsVKVX8vRUkwxOu1IT6WIepgLnP0OZT7cFjgrC1SV/16c3YLw2NZDaVe0M/H4rpeNWW30VyrzZVhyw== +"@elastic/charts@18.4.1": + version "18.4.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.4.1.tgz#19d82c39aef347fd00b33e33b68b683ac4d745b0" + integrity sha512-vV5AAKIKbwgY923OD2Rsr77XFHmsUsWWg/aeCZvG5/b6Yb+fNgM0RF94GADiDMvRvQANhTn2CPPVvNfL18MegQ== dependencies: classnames "^2.2.6" d3-array "^1.2.4" @@ -1314,16 +1306,16 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@21.0.1": - version "21.0.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-21.0.1.tgz#7cf6846ed88032aebd72f75255298df2fbe26554" - integrity sha512-Hf8ZGRI265qpOKwnnqhZkaMQvali+Xg6FAaNZSskkpXvdLhwGtUGC4YU7HW2vb7svq6IpNUuz+5XWrMLLzVY9w== +"@elastic/eui@22.3.0": + version "22.3.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-22.3.0.tgz#31a7a0aaf69b329acff2791fca677a824b63539d" + integrity sha512-LoQd11RoD6cbDuVQhwTr3lR4Jga8D5cBGaKFPzaS8MoxW5vCu0gcosjI6O5SqmCwifyfU4JP5zeOjy8TawJFxw== dependencies: - "@types/chroma-js" "^1.4.3" + "@types/chroma-js" "^2.0.0" "@types/enzyme" "^3.1.13" "@types/lodash" "^4.14.116" "@types/numeral" "^0.0.25" - "@types/react-beautiful-dnd" "^10.1.0" + "@types/react-beautiful-dnd" "^12.1.2" "@types/react-input-autosize" "^2.0.2" "@types/react-virtualized" "^9.18.7" chroma-js "^2.0.4" @@ -1335,7 +1327,7 @@ numeral "^2.0.6" prop-types "^15.6.0" react-ace "^7.0.5" - react-beautiful-dnd "^10.1.0" + react-beautiful-dnd "^13.0.0" react-focus-lock "^1.17.7" react-input-autosize "^2.2.2" react-is "~16.3.0" @@ -1385,10 +1377,10 @@ through2 "^2.0.0" update-notifier "^0.5.0" -"@elastic/maki@6.2.0": - version "6.2.0" - resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.2.0.tgz#d0a85aa248bdc14dca44e1f9430c0b670f65e489" - integrity sha512-QkmRNpEY4Dy6eqwDimR5X9leMgdPFjdANmpEIwEW1XVUG2U4YtB2BXhDxsnMmNTUrJUjtnjnwgwBUyg0pU0FTg== +"@elastic/maki@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.3.0.tgz#09780650f1510554bef9121b9db86ce297f021f1" + integrity sha512-a2U2DaemIJaW+3nL/sN/+JScdrkoggoGHLDtRPurk2Axnpa9O9QHekmMXLO7eLK1brDpYcplqGE6hwFaMvRRUg== "@elastic/node-crypto@1.1.1": version "1.1.1" @@ -3767,10 +3759,10 @@ resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.2.tgz#3152c8dedfa8621f1ccaaabb40722a8aca808bcf" integrity sha512-Ni8yCN1vF0yfnfKf5bNrBm+92EdZIX2sUk+A4t4QvO1x/9G04rGyC0nik4i5UcNfx8Q7MhX4XUDcy2nrkKQLFg== -"@types/chroma-js@^1.4.3": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.3.tgz#4456e5cb46885a4952324e55a4b6d4064904790c" - integrity sha512-m33zg9cRLtuaUSzlbMrr7iLIKNzrD4+M6Unt5+9mCu4BhR5NwnRjVKblINCwzcBXooukIgld8DtEncP8qpvbNg== +"@types/chroma-js@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.0.0.tgz#b0fc98c8625d963f14e8138e0a7961103303ab22" + integrity sha512-iomunXsXjDxhm2y1OeJt8NwmgC7RyNkPAOddlYVGsbGoX8+1jYt84SG4/tf6RWcwzROLx1kPXPE95by1s+ebIg== "@types/chromedriver@^2.38.0": version "2.38.0" @@ -4546,13 +4538,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-beautiful-dnd@^10.1.0": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#7afae39a4247f30c13b8bbb726ccd1b8cda9d4a5" - integrity sha512-75XELhEIWKTkyd1GdVZFvS1MtJwDs9tM37BbIat8mevcw+uH5dcJzZiwESHIWAzySHawS48nkKCQk/bEDp13Mw== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^12.1.1": version "12.1.1" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#149e638c0f912eee6b74ea419b26bb43d0b1da60" @@ -4560,6 +4545,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@^12.1.2": + version "12.1.2" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.2.tgz#dfd1bdb072e92c1363e5f7a4c1842eaf95f77b21" + integrity sha512-h+0mA4cHmzL4BhyCniB6ZSSZhfO9LpXXbnhdAfa2k7klS03woiOT+Dh5AchY6eoQXk3vQVtqn40YY3u+MwFs8A== + dependencies: + "@types/react" "*" + "@types/react-color@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0" @@ -7133,6 +7125,14 @@ babel-plugin-transform-define@^1.3.1: lodash "^4.17.11" traverse "0.6.6" +babel-plugin-transform-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-imports/-/babel-plugin-transform-imports-2.0.0.tgz#9e5f49f751a9d34ba8f4bb988c7e48ed2419c6b6" + integrity sha512-65ewumYJ85QiXdcB/jmiU0y0jg6eL6CdnDqQAqQ8JMOKh1E52VPG3NJzbVKWcgovUR5GBH8IWpCXQ7I8Q3wjgw== + dependencies: + "@babel/types" "^7.4" + is-valid-path "^0.1.1" + babel-plugin-transform-inline-consecutive-adds@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1" @@ -9873,7 +9873,7 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.5.7, core-js@^2.6.5, core-js@^2.6.9: +core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -10206,18 +10206,6 @@ cson@5.1.0: requirefresh "^2.1.0" safefs "^4.1.0" -css-box-model@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.0.0.tgz#60142814f2b25be00c4aac65ea1a55a531b18922" - integrity sha512-MGipbCM6/HGmsOwN6Enq1OvNKy8H5Q1XKoyBszxwv2efly7ZVg+HcFILX8O6S0xfj27l1+6P7FyCjcQ90m5HBQ== - -css-box-model@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.1.1.tgz#c9fd8e7a8b1d59d41d6812fd1765433f671b2ee0" - integrity sha512-ZxbuLFeAPEDb0wPbGfT7783Vb00MVAkvOlMKwr0kA2PD5EGxk6P3MAhedvVuyVJCWb54bb+6HQ7pdPYENf8AZw== - dependencies: - tiny-invariant "^1.0.3" - css-box-model@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0" @@ -15623,14 +15611,15 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.5.3, handlebars@^4.0.1, handlebars@^4.1.2: - version "4.5.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" - integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== +handlebars@4.7.6, handlebars@^4.0.1, handlebars@^4.1.2: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== dependencies: + minimist "^1.2.5" neo-async "^2.6.0" - optimist "^0.6.1" source-map "^0.6.1" + wordwrap "^1.0.0" optionalDependencies: uglify-js "^3.1.4" @@ -17599,7 +17588,7 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= -is-valid-path@0.1.1: +is-valid-path@0.1.1, is-valid-path@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df" integrity sha1-EQ+f90w39mPh7HkV60UfLbk6yd8= @@ -20305,21 +20294,11 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^1.1.0" -memoize-one@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.2.tgz#3fb8db695aa14ab9c0f1644e1585a8806adc1aee" - integrity sha512-ucx2DmXTeZTsS4GPPUZCbULAN7kdPT1G+H49Y34JjbQ5ESc6OGhVxKvb1iKhr9v19ZB9OtnHwNnhUnNR/7Wteg== - memoize-one@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.1.tgz#35a709ffb6e5f0cb79f9679a96f09ec3a35addfa" integrity sha512-S3plzyksLOSF4pkf1Xlb7mA8ZRKZlgp3ebg7rULbfwPT8Ww7uZz5CbLgRKaR92GeXpsNiFbfCRWf/uOrCYIbRg== -memoize-one@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.2.tgz#6aba5276856d72fb44ead3efab86432f94ba203d" - integrity sha512-o7lldN4fs/axqctc03NF+PMhd2veRrWeJ2n2GjEzUPBD4F9rmNg4A+bQCACIzwjHJEXuYv4aFFMaH35KZfHUrw== - memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -23618,15 +23597,6 @@ prop-types@15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@15.6.1: - version "15.6.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" - integrity sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ== - dependencies: - fbjs "^0.8.16" - loose-envify "^1.3.1" - object-assign "^4.1.1" - prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -24251,20 +24221,6 @@ react-apollo@^2.1.4: lodash "^4.17.10" prop-types "^15.6.0" -react-beautiful-dnd@^10.1.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#d753088d77d7632e77cf8a8935fafcffa38f574b" - integrity sha512-TdE06Shfp56wm28EzjgC56EEMgGI5PDHejJ2bxuAZvZr8CVsbksklsJC06Hxf0MSL7FHbflL/RpkJck9isuxHg== - dependencies: - "@babel/runtime-corejs2" "^7.4.2" - css-box-model "^1.1.1" - memoize-one "^5.0.1" - prop-types "^15.6.1" - raf-schd "^4.0.0" - react-redux "^5.0.7" - redux "^4.0.1" - tiny-invariant "^1.0.4" - react-beautiful-dnd@^12.2.0: version "12.2.0" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.2.0.tgz#e5f6222f9e7934c6ed4ee09024547f9e353ae423" @@ -24278,20 +24234,18 @@ react-beautiful-dnd@^12.2.0: redux "^4.0.4" use-memo-one "^1.1.1" -react-beautiful-dnd@^8.0.7: - version "8.0.7" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43" - integrity sha512-j2cClhKuACXp/KcG+YXSrVxZ7AQl13dG9X+ojstR6H2G0yoA+1GZn/O147PWVVScmfk/mSt60GNseH7vjae7vQ== +react-beautiful-dnd@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" + integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg== dependencies: - "@babel/runtime" "7.0.0-beta.54" - css-box-model "^1.0.0" - memoize-one "^4.0.0" - prop-types "15.6.1" - raf-schd "^4.0.0" - react-motion "^0.5.2" - react-redux "^5.0.7" - redux "^4.0.0" - tiny-invariant "^0.0.3" + "@babel/runtime" "^7.8.4" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" react-clientside-effect@^1.2.0: version "1.2.0" @@ -24658,15 +24612,6 @@ react-motion@^0.4.8: prop-types "^15.5.8" raf "^3.1.0" -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - react-onclickoutside@^6.5.0: version "6.7.1" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93" @@ -24733,7 +24678,7 @@ react-portal@^3.2.0: dependencies: prop-types "^15.5.8" -react-redux@^5.0.7, react-redux@^5.1.2: +react-redux@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== @@ -25456,14 +25401,6 @@ redux@^4.0.0: loose-envify "^1.1.0" symbol-observable "^1.2.0" -redux@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" - integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg== - dependencies: - loose-envify "^1.4.0" - symbol-observable "^1.2.0" - redux@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" @@ -29092,12 +29029,7 @@ tiny-inflate@^1.0.0, tiny-inflate@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.2.tgz#93d9decffc8805bd57eae4310f0b745e9b6fb3a7" integrity sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c= -tiny-invariant@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-0.0.3.tgz#4c7283c950e290889e9e94f64d3586ec9156cf44" - integrity sha512-SA2YwvDrCITM9fTvHTHRpq9W6L2fBsClbqm3maT5PZux4Z73SPPDYwJMtnoWh6WMgmCkJij/LaOlWiqJqFMK8g== - -tiny-invariant@^1.0.2, tiny-invariant@^1.0.3, tiny-invariant@^1.0.4: +tiny-invariant@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g==