diff --git a/.eslintrc.js b/.eslintrc.js
index 40dd6a55a2a3f..c64f03a8398e5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -893,6 +893,8 @@ module.exports = {
files: [
'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}',
],
rules: {
'import/no-nodejs-modules': 'error',
@@ -907,7 +909,10 @@ module.exports = {
},
{
// typescript only for front and back end
- files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'],
+ files: [
+ 'x-pack/plugins/security_solution/**/*.{ts,tsx}',
+ 'x-pack/plugins/timelines/**/*.{ts,tsx}',
+ ],
rules: {
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-explicit-any': 'error',
@@ -917,7 +922,10 @@ module.exports = {
},
{
// typescript and javascript for front and back end
- files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'],
+ files: [
+ 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}',
+ ],
plugins: ['eslint-plugin-node', 'react'],
env: {
jest: true,
diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc
index 267ab3891d700..5bd3a7587dde9 100644
--- a/docs/api/saved-objects/bulk_create.asciidoc
+++ b/docs/api/saved-objects/bulk_create.asciidoc
@@ -45,6 +45,11 @@ experimental[] Create multiple {kib} saved objects.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
`version`::
(Optional, number) Specifies the version.
diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc
index d7a368034ef07..e7e25c7d3bba6 100644
--- a/docs/api/saved-objects/create.asciidoc
+++ b/docs/api/saved-objects/create.asciidoc
@@ -52,6 +52,11 @@ any data that you send to the API is properly formed.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'): this option cannot be used.
[[saved-objects-api-create-request-codes]]
==== Response code
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index e8b950a696f55..5d7ba22841aa1 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -86,6 +86,7 @@ yarn kbn watch-bazel
- @kbn/logging
- @kbn/mapbox-gl
- @kbn/monaco
+- @kbn/optimizer
- @kbn/rule-data-utils
- @kbn/securitysolution-es-utils
- @kbn/securitysolution-hook-utils
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
index d3d76079cdc2a..b10ad949c4944 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
@@ -106,6 +106,7 @@ readonly links: {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -116,6 +117,7 @@ readonly links: {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
index 34279cef198bf..c020f57faa882 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
@@ -17,5 +17,5 @@ export interface DocLinksStart
| --- | --- | --- |
| [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string
| |
| [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string
| |
-| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
}
| |
+| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
}
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
index 3db8bbadfbd6b..4d094ecde7a96 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
index 6fc01212a2e41..463c3fe81b702 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
@@ -18,7 +18,7 @@ export interface SavedObjectsBulkCreateObject
| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T
| |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string
| |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[]
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
index 262b0997cb905..43489b8d2e8a2 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
index 1805f389d4e7f..7eaa9c51f5c82 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
@@ -17,7 +17,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string
| (not recommended) Specify an id for the document |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean
| Overwrite existing documents (defaults to false) |
diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md
index 143cd397c40ae..bf08ca1682f3b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md
+++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md
@@ -24,5 +24,7 @@ set(status$: Observable): void;
## Remarks
+The first emission from this Observable should occur within 30s, else this plugin's status will fallback to `unavailable` until the first emission.
+
See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core.
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
index 54b5a33ccf682..2ca4847d6dc39 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md
@@ -13,11 +13,11 @@ esFilters: {
FILTERS: typeof FILTERS;
FilterStateStore: typeof FilterStateStore;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter;
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter;
isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter;
isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter;
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md
index 2cde2b7455585..881a1fa803ca6 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md
@@ -10,6 +10,6 @@
esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
}
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md
index 2430e6a93bd2b..70805aaaaee8c 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md
@@ -10,7 +10,7 @@
esQuery: {
buildEsQuery: typeof buildEsQuery;
getEsQueryConfig: typeof getEsQueryConfig;
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md
deleted file mode 100644
index 792bee44f96a8..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md)
-
-## IIndexPattern.fields property
-
-Signature:
-
-```typescript
-fields: IFieldType[];
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md
deleted file mode 100644
index 917a80975df6c..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md)
-
-## IIndexPattern.id property
-
-Signature:
-
-```typescript
-id?: string;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
index bf7f88ab37039..88d8520a373c6 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
@@ -12,7 +12,7 @@
Signature:
```typescript
-export interface IIndexPattern
+export interface IIndexPattern extends MinimalIndexPattern
```
## Properties
@@ -20,9 +20,7 @@ export interface IIndexPattern
| Property | Type | Description |
| --- | --- | --- |
| [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, SerializedFieldFormat<unknown> | undefined>
| |
-| [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | IFieldType[]
| |
| [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat
| Look up a formatter for a given field |
-| [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | string
| |
| [timeFieldName](./kibana-plugin-plugins-data-public.iindexpattern.timefieldname.md) | string
| |
| [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string
| |
| [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string
| Type is used for identifying rollup indices, otherwise left undefined |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md
new file mode 100644
index 0000000000000..d649212ae0547
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) > [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md)
+
+## IKibanaSearchResponse.isRestored property
+
+Indicates whether the results returned are from the async-search index
+
+Signature:
+
+```typescript
+isRestored?: boolean;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md
index 1d3e0c08dfc18..c7046902dac72 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md
@@ -16,6 +16,7 @@ export interface IKibanaSearchResponse
| --- | --- | --- |
| [id](./kibana-plugin-plugins-data-public.ikibanasearchresponse.id.md) | string
| Some responses may contain a unique id to identify the request this response came from. |
| [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | boolean
| Indicates whether the results returned are complete or partial |
+| [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) | boolean
| Indicates whether the results returned are from the async-search index |
| [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | boolean
| Indicates whether search is still in flight |
| [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | number
| If relevant to the search strategy, return a loaded number that represents how progress is indicated. |
| [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | RawResponse
| The raw response returned by the internal search method (usually the raw ES response) |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md
index d7e80d94db4e6..d951cb2426943 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md
@@ -11,11 +11,11 @@ esFilters: {
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
buildCustomFilter: typeof buildCustomFilter;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter;
buildFilter: typeof buildFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
isFilterDisabled: (filter: import("../common").Filter) => boolean;
}
```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md
index 4b96d8af756f3..6274eb5f4f4a5 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md
@@ -10,6 +10,6 @@
esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
}
```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md
index ac9be23bc6b6f..0d1baecb014f5 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md
@@ -8,7 +8,7 @@
```typescript
esQuery: {
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
index b1745b298e27e..9816b884c4614 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
@@ -13,6 +13,7 @@
| [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | |
| [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | |
| [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | |
+| [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) | |
| [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | |
| [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md
new file mode 100644
index 0000000000000..e48a1c98f8578
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) > [(constructor)](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md)
+
+## NoSearchIdInSessionError.(constructor)
+
+Constructs a new instance of the `NoSearchIdInSessionError` class
+
+Signature:
+
+```typescript
+constructor();
+```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md
new file mode 100644
index 0000000000000..707739f845cd1
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md)
+
+## NoSearchIdInSessionError class
+
+Signature:
+
+```typescript
+export declare class NoSearchIdInSessionError extends KbnError
+```
+
+## Constructors
+
+| Constructor | Modifiers | Description |
+| --- | --- | --- |
+| [(constructor)()](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) | | Constructs a new instance of the NoSearchIdInSessionError
class |
+
diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc
index 65b600d4b7281..3d3d7aeb2d777 100644
--- a/docs/management/action-types.asciidoc
+++ b/docs/management/action-types.asciidoc
@@ -43,6 +43,10 @@ a| <>
| Send a message to a Slack channel or user.
+a| <>
+
+| Create an incident in Swimlane.
+
a| <>
| Send a request to a web service.
diff --git a/docs/management/connectors/action-types/swimlane.asciidoc b/docs/management/connectors/action-types/swimlane.asciidoc
new file mode 100644
index 0000000000000..88447bb496a86
--- /dev/null
+++ b/docs/management/connectors/action-types/swimlane.asciidoc
@@ -0,0 +1,105 @@
+[role="xpack"]
+[[swimlane-action-type]]
+=== Swimlane connector and action
+++++
+Swimlane
+++++
+
+The Swimlane connector uses the https://swimlane.com/knowledge-center/docs/developer-guide/rest-api/[Swimlane REST API] to create Swimlane records.
+
+[float]
+[[swimlane-connector-configuration]]
+==== Connector configuration
+
+Swimlane connectors have the following configuration properties.
+
+Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action.
+URL:: Swimlane instance URL.
+Application ID:: Swimlane application ID.
+API token:: Swimlane API authentication token for HTTP Basic authentication.
+
+[float]
+[[Preconfigured-swimlane-configuration]]
+==== Preconfigured connector type
+
+[source,text]
+--
+ my-swimlane:
+ name: preconfigured-swimlane-connector-type
+ actionTypeId: .swimlane
+ config:
+ apiUrl: https://elastic.swimlaneurl.us
+ appId: app-id
+ mappings:
+ alertIdConfig:
+ fieldType: text
+ id: agp4s
+ key: alert-id
+ name: Alert ID
+ caseIdConfig:
+ fieldType: text
+ id: ae1mi
+ key: case-id
+ name: Case ID
+ caseNameConfig:
+ fieldType: text
+ id: anxnr
+ key: case-name
+ name: Case Name
+ commentsConfig:
+ fieldType: comments
+ id: au18d
+ key: comments
+ name: Comments
+ descriptionConfig:
+ fieldType: text
+ id: ae1gd
+ key: description
+ name: Description
+ ruleNameConfig:
+ fieldType: text
+ id: avfsl
+ key: rule-name
+ name: Rule Name
+ severityConfig:
+ fieldType: text
+ id: a71ik
+ key: severity
+ name: severity
+ secrets:
+ apiToken: tokenkeystorevalue
+--
+
+Config defines information for the connector type.
+
+`apiUrl`:: An address that corresponds to *URL*.
+`appId`:: A key that corresponds to *Application ID*.
+
+Secrets defines sensitive information for the connector type.
+
+`apiToken`:: A string that corresponds to *API Token*. Should be stored in the <>.
+
+[float]
+[[define-swimlane-ui]]
+==== Define connector in Stack Management
+
+Define Swimlane connector properties.
+
+[role="screenshot"]
+image::management/connectors/images/swimlane-connector.png[Swimlane connector]
+
+Test Swimlane action parameters.
+
+[role="screenshot"]
+image::management/connectors/images/swimlane-params-test.png[Swimlane params test]
+
+[float]
+[[swimlane-action-configuration]]
+==== Action configuration
+
+Swimlane actions have the following configuration properties.
+
+Comments:: Additional information for the client, such as how to troubleshoot the issue.
+Severity:: The severity of the incident.
+
+NOTE: Alert ID and Rule Name are filled automatically. Specifically, Alert ID is set to `{{alert.id}}` and Rule Name to `{{rule.name}}`.
\ No newline at end of file
diff --git a/docs/management/connectors/images/swimlane-connector.png b/docs/management/connectors/images/swimlane-connector.png
new file mode 100644
index 0000000000000..520c35d00381b
Binary files /dev/null and b/docs/management/connectors/images/swimlane-connector.png differ
diff --git a/docs/management/connectors/images/swimlane-params-test.png b/docs/management/connectors/images/swimlane-params-test.png
new file mode 100644
index 0000000000000..c0e02c2c7b18f
Binary files /dev/null and b/docs/management/connectors/images/swimlane-params-test.png differ
diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc
index ea4fa46d3e808..033b1c3ac150e 100644
--- a/docs/management/connectors/index.asciidoc
+++ b/docs/management/connectors/index.asciidoc
@@ -6,6 +6,7 @@ include::action-types/teams.asciidoc[]
include::action-types/pagerduty.asciidoc[]
include::action-types/server-log.asciidoc[]
include::action-types/servicenow.asciidoc[]
+include::action-types/swimlane.asciidoc[]
include::action-types/slack.asciidoc[]
include::action-types/webhook.asciidoc[]
include::pre-configured-connectors.asciidoc[]
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index 71f141d1ed5d6..d1d283ca60fbb 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -69,7 +69,7 @@ You can configure the following settings in the `kibana.yml` file.
--
xpack.actions.customHostSettings:
- url: smtp://mail.example.com:465
- tls:
+ ssl:
verificationMode: 'full'
certificateAuthoritiesFiles: [ 'one.crt' ]
certificateAuthoritiesData: |
@@ -79,7 +79,7 @@ xpack.actions.customHostSettings:
smtp:
requireTLS: true
- url: https://webhook.example.com
- tls:
+ ssl:
// legacy
rejectUnauthorized: false
verificationMode: 'none'
@@ -97,8 +97,8 @@ xpack.actions.customHostSettings:
server, and the `https` URLs are used for actions which use `https` to
connect to services. +
+
- Entries with `https` URLs can use the `tls` options, and entries with `smtp`
- URLs can use both the `tls` and `smtp` options. +
+ Entries with `https` URLs can use the `ssl` options, and entries with `smtp`
+ URLs can use both the `ssl` and `smtp` options. +
+
No other URL values should be part of this URL, including paths,
query strings, and authentication information. When an http or smtp request
@@ -117,24 +117,24 @@ xpack.actions.customHostSettings:
The options `smtp.ignoreTLS` and `smtp.requireTLS` can not both be set to true.
| `xpack.actions.customHostSettings[n]`
-`.tls.rejectUnauthorized` {ess-icon}
- | Deprecated. Use <> instead. A boolean value indicating whether to bypass server certificate validation.
+`.ssl.rejectUnauthorized` {ess-icon}
+ | Deprecated. Use <> instead. A boolean value indicating whether to bypass server certificate validation.
Overrides the general `xpack.actions.rejectUnauthorized` configuration
for requests made for this hostname/port.
|[[action-config-custom-host-verification-mode]] `xpack.actions.customHostSettings[n]`
-`.tls.verificationMode`
+`.ssl.verificationMode`
| Controls the verification of the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the host server. Valid values are `full`, `certificate`, and `none`.
- Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. Overrides the general `xpack.actions.tls.verificationMode` configuration
+ Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. Overrides the general `xpack.actions.ssl.verificationMode` configuration
for requests made for this hostname/port.
| `xpack.actions.customHostSettings[n]`
-`.tls.certificateAuthoritiesFiles`
+`.ssl.certificateAuthoritiesFiles`
| A file name or list of file names of PEM-encoded certificate files to use
to validate the server.
| `xpack.actions.customHostSettings[n]`
-`.tls.certificateAuthoritiesData` {ess-icon}
+`.ssl.certificateAuthoritiesData` {ess-icon}
| The contents of a PEM-encoded certificate file, or multiple files appended
into a single string. This configuration can be used for environments where
the files cannot be made available.
@@ -165,28 +165,28 @@ xpack.actions.customHostSettings:
a|`xpack.actions.`
`proxyRejectUnauthorizedCertificates` {ess-icon}
- | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`.
+ | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`.
|[[action-config-proxy-verification-mode]]
`xpack.actions[n]`
-`.tls.proxyVerificationMode` {ess-icon}
+`.ssl.proxyVerificationMode` {ess-icon}
| Controls the verification for the proxy server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the proxy server. Valid values are `full`, `certificate`, and `none`.
Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>.
| `xpack.actions.rejectUnauthorized` {ess-icon}
- | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. +
+ | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. +
+
As an alternative to setting `xpack.actions.rejectUnauthorized`, you can use the setting
- `xpack.actions.customHostSettings` to set TLS options for specific servers.
+ `xpack.actions.customHostSettings` to set SSL options for specific servers.
|[[action-config-verification-mode]]
`xpack.actions[n]`
-`.tls.verificationMode` {ess-icon}
+`.ssl.verificationMode` {ess-icon}
| Controls the verification for the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection for actions. Valid values are `full`, `certificate`, and `none`.
Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. +
+
- As an alternative to setting `xpack.actions.tls.verificationMode`, you can use the setting
- `xpack.actions.customHostSettings` to set TLS options for specific servers.
+ As an alternative to setting `xpack.actions.ssl.verificationMode`, you can use the setting
+ `xpack.actions.customHostSettings` to set SSL options for specific servers.
diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc
index cb5c484def3b9..17bfc19c2e0c9 100644
--- a/docs/user/dashboard/aggregation-reference.asciidoc
+++ b/docs/user/dashboard/aggregation-reference.asciidoc
@@ -12,91 +12,168 @@ This reference can help simplify the comparison if you need a specific feature.
[options="header"]
|===
-| Type | Aggregation-based | Lens | TSVB | Timelion | Vega
+| Type | Lens | TSVB | Agg-based | Vega | Timelion
| Table
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
|
|
-| Table with summary row
-^| X
-^| X
-|
+| Bar, line, and area
+| ✓
+| ✓
+| ✓
+| ✓
+| ✓
+
+| Split chart/small multiples
|
+| ✓
+| ✓
+| ✓
|
-| Bar, line, and area charts
-^| X
-^| X
-^| X
-^| X
-^| X
+| Pie and donut
+| ✓
+|
+| ✓
+| ✓
+|
-| Percentage bar or area chart
+| Sunburst
+| ✓
|
-^| X
-^| X
+| ✓
+| ✓
|
-^| X
-| Split bar, line, and area charts
-^| X
+| Treemap
+| ✓
+|
|
+| ✓
|
+
+| Heat map
+| ✓
+| ✓
+| ✓
+| ✓
|
-^| X
-| Pie and donut charts
-^| X
-^| X
+| Gauge and Goal
|
+| ✓
+| ✓
+| ✓
|
-^| X
-| Sunburst chart
-^| X
-^| X
+| Markdown
+|
+| ✓
|
|
|
-| Heat map
-^| X
-^| X
+| Metric
+| ✓
+| ✓
+| ✓
+| ✓
+|
+
+| Tag cloud
|
|
-^| X
+| ✓
+| ✓
+|
-| Gauge and Goal
-^| X
+|===
+
+[float]
+[[table-features]]
+=== Table features
+
+[options="header"]
+|===
+
+| Type | Lens | TSVB | Agg-based
+
+| Summary row
+| ✓
|
-^| X
+| ✓
+
+| Pivot table
+| ✓
|
|
-| Markdown
+| Calculated column
+| Formula
+| ✓
+| Percent only
+
+| Color by value
+| ✓
+| ✓
|
+
+|===
+
+[float]
+[[xy-features]]
+=== Bar, line, area features
+
+[options="header"]
+|===
+
+| Type | Lens | TSVB | Agg-based | Vega | Timelion
+
+| Dense time series
+| Customizable
+| ✓
+| Customizable
+| ✓
+| ✓
+
+| Percentage mode
+| ✓
+| ✓
+| ✓
+| ✓
|
-^| X
+
+| Break downs
+| 1
+| 1
+| 3
+| ∞
+| 1
+
+| Custom color with break downs
|
+| Only for Filters
+| ✓
+| ✓
|
-| Metric
-^| X
-^| X
-^| X
+| Fit missing values
+| ✓
|
-^| X
+| ✓
+| ✓
+| ✓
-| Tag cloud
-^| X
+| Synchronized tooltips
+|
+| ✓
|
|
|
-^| X
|===
@@ -111,67 +188,57 @@ For information about {es} bucket aggregations, refer to {ref}/search-aggregatio
[options="header"]
|===
-| Type | Agg-based | Markdown | Lens | TSVB
+| Type | Lens | TSVB | Agg-based
| Histogram
-^| X
-^| X
-^| X
+| ✓
|
+| ✓
| Date histogram
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Date range
-^| X
-^| X
-|
+| Use filters
|
+| ✓
| Filter
-^| X
-^| X
|
-^| X
+| ✓
+|
| Filters
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| GeoHash grid
-^| X
-^| X
|
|
+| ✓
| IP range
-^| X
-^| X
-|
-|
+| Use filters
+| Use filters
+| ✓
| Range
-^| X
-^| X
-^| X
-|
+| ✓
+| Use filters
+| ✓
| Terms
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Significant terms
-^| X
-^| X
|
-^| X
+|
+| ✓
|===
@@ -186,67 +253,57 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati
[options="header"]
|===
-| Type | Agg-based | Markdown | Lens | TSVB
+| Type | Lens | TSVB | Agg-based
| Metrics with filters
+| ✓
|
|
-^| X
-|
-
-| Average
-^| X
-^| X
-^| X
-^| X
-| Sum
-^| X
-^| X
-^| X
-^| X
+| Average, Sum, Max, Min
+| ✓
+| ✓
+| ✓
| Unique count (Cardinality)
-^| X
-^| X
-^| X
-^| X
-
-| Max
-^| X
-^| X
-^| X
-^| X
-
-| Min
-^| X
-^| X
-^| X
-^| X
-
-| Percentiles
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
+
+| Percentiles and Median
+| ✓
+| ✓
+| ✓
| Percentiles Rank
-^| X
-^| X
-|
-^| X
+|
+| ✓
+| ✓
+
+| Standard deviation
+|
+| ✓
+| ✓
+
+| Sum of squares
+|
+| ✓
+|
| Top hit (Last value)
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Value count
|
|
+| ✓
+
+| Variance
+|
+| ✓
|
-^| X
|===
@@ -261,61 +318,94 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat
[options="header"]
|===
-| Type | Agg-based | Markdown | Lens | TSVB
+| Type | Lens | TSVB | Agg-based
| Avg bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Derivative
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Max bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Min bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Sum bucket
-^| X
-^| X
-|
-^| X
+| <>
+| ✓
+| ✓
| Moving average
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Cumulative sum
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
+| ✓
| Bucket script
|
|
+| ✓
+
+| Bucket selector
+|
|
-^| X
+|
| Serial differencing
-^| X
-^| X
|
-^| X
+| ✓
+| ✓
+
+|===
+
+[float]
+[[custom-functions]]
+=== Additional functions
+
+[options="header"]
+|===
+
+| Type | Lens | TSVB | Agg-based
+
+| Counter rate
+| ✓
+| ✓
+|
+
+| <>
+| Use <>
+| ✓
+|
+
+| <>
+|
+| ✓
+|
+
+| <>
+|
+| ✓
+|
+
+| Static value
+|
+| ✓
+|
+
|===
@@ -329,41 +419,49 @@ build their advanced visualization.
[options="header"]
|===
-| Type | Agg-based | Lens | TSVB | Timelion | Vega
+| Type | Lens | TSVB | Agg-based | Vega | Timelion
-| Math on aggregated data
+| Math
+| ✓
+| ✓
|
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
| Visualize two indices
+| ✓
+| ✓
|
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
| Math across indices
|
|
|
-^| X
-^| X
+| ✓
+| ✓
| Time shifts
+| ✓
+| ✓
|
-^| X
-^| X
-^| X
-^| X
+| ✓
+| ✓
| Fully custom {es} queries
|
|
|
+| ✓
|
-^| X
+
+| Normalize by time
+| ✓
+| ✓
+|
+|
+|
+
|===
diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc
index 4ecfcc9250122..2071f17ecff3d 100644
--- a/docs/user/dashboard/lens.asciidoc
+++ b/docs/user/dashboard/lens.asciidoc
@@ -139,6 +139,42 @@ image::images/lens_drag_drop_3.gif[Using drag and drop to reorder]
. Press Space bar to confirm, or to cancel, press Esc.
+[float]
+[[lens-formulas]]
+==== Use formulas to perform math
+
+Formulas let you perform math on aggregated data in Lens by typing
+math and quick functions. To access formulas,
+click the *Formula* tab in the dimension editor. Access the complete
+reference for formulas from the help menu.
+
+The most common formulas are dividing two values to produce a percent.
+To display accurately, set *Value format* to *Percent*.
+
+Filter ratio::
+
+Use `kql=''` to filter one set of documents and compare it to other documents within the same grouping.
+For example, to see how the error rate changes over time:
++
+```
+count(kql='response.status_code > 400') / count()
+```
+
+Week over week:: Use `shift='1w'` to get the value of each grouping from
+the previous week. Time shift should not be used with the *Top values* function.
++
+```
+percentile(system.network.in.bytes, percentile=99) /
+percentile(system.network.in.bytes, percentile=99, shift='1w')
+```
+
+Percent of total:: Formulas can calculate `overall_sum` for all the groupings,
+which lets you convert each grouping into a percent of total:
++
+```
+sum(products.base_price) / overall_sum(sum(products.base_price))
+```
+
[float]
[[lens-faq]]
==== Frequently asked questions
diff --git a/package.json b/package.json
index 36fa086657adf..f99eb86a43cec 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
"@elastic/datemath": "link:bazel-bin/packages/elastic-datemath",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13",
"@elastic/ems-client": "7.14.0",
- "@elastic/eui": "33.0.0",
+ "@elastic/eui": "34.3.0",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "^9.0.1-kibana3",
"@elastic/maki": "6.3.0",
@@ -149,6 +149,7 @@
"@kbn/securitysolution-list-api": "link:bazel-bin/packages/kbn-securitysolution-list-api",
"@kbn/securitysolution-list-hooks": "link:bazel-bin/packages/kbn-securitysolution-list-hooks",
"@kbn/securitysolution-list-utils": "link:bazel-bin/packages/kbn-securitysolution-list-utils",
+ "@kbn/securitysolution-t-grid": "link:bazel-bin/packages/kbn-securitysolution-t-grid",
"@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
@@ -217,6 +218,8 @@
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
"d3-array": "1.2.4",
+ "d3-cloud": "1.2.5",
+ "d3-interpolate": "^3.0.1",
"d3-scale": "1.0.7",
"d3-shape": "^1.1.0",
"d3-time": "^1.1.0",
@@ -462,7 +465,7 @@
"@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana",
"@kbn/eslint-plugin-eslint": "link:bazel-bin/packages/kbn-eslint-plugin-eslint",
"@kbn/expect": "link:bazel-bin/packages/kbn-expect",
- "@kbn/optimizer": "link:packages/kbn-optimizer",
+ "@kbn/optimizer": "link:bazel-bin/packages/kbn-optimizer",
"@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator",
"@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers",
"@kbn/pm": "link:packages/kbn-pm",
@@ -511,6 +514,7 @@
"@types/cytoscape": "^3.14.0",
"@types/d3": "^3.5.43",
"@types/d3-array": "^1.2.7",
+ "@types/d3-interpolate": "^2.0.0",
"@types/d3-scale": "^2.1.1",
"@types/d3-shape": "^1.3.1",
"@types/d3-time": "^1.0.10",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index b1c3f580c6baf..d9e2f0e1f9985 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -3,7 +3,7 @@
filegroup(
name = "build",
srcs = [
- "//packages/elastic-datemath:build",
+ "//packages/elastic-datemath:build",
"//packages/elastic-eslint-config-kibana:build",
"//packages/elastic-safer-lodash-set:build",
"//packages/kbn-ace:build",
@@ -29,6 +29,7 @@ filegroup(
"//packages/kbn-logging:build",
"//packages/kbn-mapbox-gl:build",
"//packages/kbn-monaco:build",
+ "//packages/kbn-optimizer:build",
"//packages/kbn-plugin-generator:build",
"//packages/kbn-rule-data-utils:build",
"//packages/kbn-securitysolution-list-constants:build",
@@ -41,6 +42,7 @@ filegroup(
"//packages/kbn-securitysolution-list-utils:build",
"//packages/kbn-securitysolution-utils:build",
"//packages/kbn-securitysolution-es-utils:build",
+ "//packages/kbn-securitysolution-t-grid:build",
"//packages/kbn-securitysolution-hook-utils:build",
"//packages/kbn-server-http-tools:build",
"//packages/kbn-server-route-repository:build",
diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json
index dd491de55c075..cf6fcfd88a26d 100644
--- a/packages/kbn-cli-dev-mode/package.json
+++ b/packages/kbn-cli-dev-mode/package.json
@@ -12,8 +12,5 @@
},
"kibana": {
"devOnly": true
- },
- "dependencies": {
- "@kbn/optimizer": "link:../kbn-optimizer"
}
}
\ No newline at end of file
diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel
new file mode 100644
index 0000000000000..3809c2b33d500
--- /dev/null
+++ b/packages/kbn-optimizer/BUILD.bazel
@@ -0,0 +1,120 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-optimizer"
+PKG_REQUIRE_NAME = "@kbn/optimizer"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*.ts",
+ ],
+ exclude = [
+ "**/*.test.*",
+ "**/__fixtures__/**",
+ "**/__snapshots__/**",
+ ],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "limits.yml",
+ "package.json",
+ "postcss.config.js",
+ "README.md"
+]
+
+SRC_DEPS = [
+ "//packages/kbn-config",
+ "//packages/kbn-dev-utils",
+ "//packages/kbn-std",
+ "//packages/kbn-ui-shared-deps",
+ "//packages/kbn-utils",
+ "@npm//chalk",
+ "@npm//clean-webpack-plugin",
+ "@npm//compression-webpack-plugin",
+ "@npm//cpy",
+ "@npm//del",
+ "@npm//execa",
+ "@npm//jest-diff",
+ "@npm//json-stable-stringify",
+ "@npm//lmdb-store",
+ "@npm//loader-utils",
+ "@npm//node-sass",
+ "@npm//normalize-path",
+ "@npm//pirates",
+ "@npm//resize-observer-polyfill",
+ "@npm//rxjs",
+ "@npm//source-map-support",
+ "@npm//watchpack",
+ "@npm//webpack",
+ "@npm//webpack-merge",
+ "@npm//webpack-sources",
+ "@npm//zlib"
+]
+
+TYPES_DEPS = [
+ "@npm//@types/compression-webpack-plugin",
+ "@npm//@types/jest",
+ "@npm//@types/json-stable-stringify",
+ "@npm//@types/loader-utils",
+ "@npm//@types/node",
+ "@npm//@types/normalize-path",
+ "@npm//@types/source-map-support",
+ "@npm//@types/watchpack",
+ "@npm//@types/webpack",
+ "@npm//@types/webpack-merge",
+ "@npm//@types/webpack-sources",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ declaration = True,
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target",
+ source_map = True,
+ root_dir = "src",
+ tsconfig = ":tsconfig",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ deps = DEPS + [":tsc"],
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ]
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index f9127e4629f43..c6960621359c7 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -67,7 +67,7 @@ pageLoadAssetSize:
searchprofiler: 67080
security: 95864
securityOss: 30806
- securitySolution: 76000
+ securitySolution: 217673
share: 99061
snapshotRestore: 79032
spaces: 57868
@@ -107,7 +107,7 @@ pageLoadAssetSize:
dataVisualizer: 27530
banners: 17946
mapsEms: 26072
- timelines: 28613
+ timelines: 230410
screenshotMode: 17856
visTypePie: 35583
cases: 144442
diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json
index a6c8284ad15f6..d23512f7c418d 100644
--- a/packages/kbn-optimizer/package.json
+++ b/packages/kbn-optimizer/package.json
@@ -4,10 +4,5 @@
"private": true,
"license": "SSPL-1.0 OR Elastic License 2.0",
"main": "./target/index.js",
- "types": "./target/index.d.ts",
- "scripts": {
- "build": "../../node_modules/.bin/tsc",
- "kbn:bootstrap": "yarn build",
- "kbn:watch": "yarn build --watch"
- }
+ "types": "./target/index.d.ts"
}
\ No newline at end of file
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 c175979f0e820..1f1e33d3dda7c 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
@@ -123,7 +123,7 @@ exports[`prepares assets for distribution: metrics.json 1`] = `
\\"group\\": \\"page load bundle size\\",
\\"id\\": \\"foo\\",
\\"value\\": 4627,
- \\"limitConfigPath\\": \\"packages/kbn-optimizer/limits.yml\\"
+ \\"limitConfigPath\\": \\"node_modules/@kbn/optimizer/limits.yml\\"
},
{
\\"group\\": \\"async chunks size\\",
diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts
index 92875d3f69e46..d9e1bee22557b 100644
--- a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts
+++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts
@@ -79,7 +79,7 @@ export class BundleMetricsPlugin {
id: bundle.id,
value: entry.size,
limit: bundle.pageLoadAssetSizeLimit,
- limitConfigPath: `packages/kbn-optimizer/limits.yml`,
+ limitConfigPath: `node_modules/@kbn/optimizer/limits.yml`,
},
{
group: `async chunks size`,
diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json
index f2d508cf14a55..76beaf7689fd4 100644
--- a/packages/kbn-optimizer/tsconfig.json
+++ b/packages/kbn-optimizer/tsconfig.json
@@ -1,10 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
- "incremental": false,
+ "incremental": true,
"outDir": "./target",
"declaration": true,
"declarationMap": true,
+ "rootDir": "./src",
"sourceMap": true,
"sourceRoot": "../../../../packages/kbn-optimizer/src"
},
diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json
index 2d642d9ede13b..36a37075191a3 100644
--- a/packages/kbn-plugin-helpers/package.json
+++ b/packages/kbn-plugin-helpers/package.json
@@ -15,8 +15,5 @@
"scripts": {
"kbn:bootstrap": "rm -rf target && ../../node_modules/.bin/tsc",
"kbn:watch": "../../node_modules/.bin/tsc --watch"
- },
- "dependencies": {
- "@kbn/optimizer": "link:../kbn-optimizer"
}
}
\ No newline at end of file
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index e455f487d1384..5be9dff630ed5 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -63827,6 +63827,7 @@ function getProjectPaths({
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/plugin_functional/plugins/*'));
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/interpreter_functional/plugins/*'));
+ projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/server_integration/__fixtures__/plugins/*'));
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'examples/*'));
if (!ossOnly) {
diff --git a/packages/kbn-pm/src/config.ts b/packages/kbn-pm/src/config.ts
index a11b2ad9c72c3..666a2fed7a33c 100644
--- a/packages/kbn-pm/src/config.ts
+++ b/packages/kbn-pm/src/config.ts
@@ -31,6 +31,7 @@ export function getProjectPaths({ rootPath, ossOnly, skipKibanaPlugins }: Option
// correct and the expect behavior.
projectPaths.push(resolve(rootPath, 'test/plugin_functional/plugins/*'));
projectPaths.push(resolve(rootPath, 'test/interpreter_functional/plugins/*'));
+ projectPaths.push(resolve(rootPath, 'test/server_integration/__fixtures__/plugins/*'));
projectPaths.push(resolve(rootPath, 'examples/*'));
if (!ossOnly) {
diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts
index f75f0dcebf4f6..1909bcb1bcc2e 100644
--- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts
+++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts
@@ -42,6 +42,7 @@ export interface UseExceptionListsProps {
notifications: NotificationsStart;
pagination?: Pagination;
showTrustedApps: boolean;
+ showEventFilters: boolean;
}
export interface UseExceptionListProps {
diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts
index a9a93aa8df49a..0bd4c6c705668 100644
--- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts
+++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts
@@ -28,6 +28,7 @@ export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination,
* @param namespaceTypes spaces to be searched
* @param notifications kibana service for displaying toasters
* @param showTrustedApps boolean - include/exclude trusted app lists
+ * @param showEventFilters boolean - include/exclude event filters lists
* @param pagination
*
*/
@@ -43,6 +44,7 @@ export const useExceptionLists = ({
namespaceTypes,
notifications,
showTrustedApps = false,
+ showEventFilters = false,
}: UseExceptionListsProps): ReturnExceptionLists => {
const [exceptionLists, setExceptionLists] = useState([]);
const [paginationInfo, setPagination] = useState(pagination);
@@ -51,8 +53,9 @@ export const useExceptionLists = ({
const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]);
const filters = useMemo(
- (): string => getFilters(filterOptions, namespaceTypes, showTrustedApps),
- [namespaceTypes, filterOptions, showTrustedApps]
+ (): string =>
+ getFilters({ filters: filterOptions, namespaceTypes, showTrustedApps, showEventFilters }),
+ [namespaceTypes, filterOptions, showTrustedApps, showEventFilters]
);
useEffect(() => {
diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts
new file mode 100644
index 0000000000000..934a9cbff56a6
--- /dev/null
+++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getEventFiltersFilter } from '.';
+
+describe('getEventFiltersFilter', () => {
+ test('it returns filter to search for "exception-list" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(true, ['exception-list']);
+
+ expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_event_filters*)');
+ });
+
+ test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(true, ['exception-list', 'exception-list-agnostic']);
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it returns filter to exclude "exception-list" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(false, ['exception-list']);
+
+ expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_event_filters*)');
+ });
+
+ test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => {
+ const filter = getEventFiltersFilter(false, ['exception-list', 'exception-list-agnostic']);
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+});
diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts
new file mode 100644
index 0000000000000..7e55073228fca
--- /dev/null
+++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants';
+import { SavedObjectType } from '../types';
+
+export const getEventFiltersFilter = (
+ showEventFilter: boolean,
+ namespaceTypes: SavedObjectType[]
+): string => {
+ if (showEventFilter) {
+ const filters = namespaceTypes.map((namespace) => {
+ return `${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`;
+ });
+ return `(${filters.join(' OR ')})`;
+ } else {
+ const filters = namespaceTypes.map((namespace) => {
+ return `not ${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`;
+ });
+ return `(${filters.join(' AND ')})`;
+ }
+};
diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts
index 327a29dc1b987..bfaad52ee8147 100644
--- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts
+++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts
@@ -11,106 +11,318 @@ import { getFilters } from '.';
describe('getFilters', () => {
describe('single', () => {
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({}, ['single'], false);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
- expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)');
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({}, ['single'], true);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
- expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)');
+ expect(filter).toEqual(
+ '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
});
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
);
});
test('it if filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it if filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)'
);
});
});
describe('agnostic', () => {
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({}, ['agnostic'], false);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({}, ['agnostic'], true);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it if filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true);
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it if filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
expect(filter).toEqual(
- '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
});
describe('single, agnostic', () => {
test('it properly formats when no filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters({}, ['single', 'agnostic'], false);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters({}, ['single', 'agnostic'], true);
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when filters passed and "showTrustedApps" is false', () => {
- const filter = getFilters(
- { created_by: 'moi', name: 'Sample' },
- ['single', 'agnostic'],
- false
- );
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it properly formats when filters passed and "showTrustedApps" is true', () => {
- const filter = getFilters(
- { created_by: 'moi', name: 'Sample' },
- ['single', 'agnostic'],
- true
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: true,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when no filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: {},
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
+
+ expect(filter).toEqual(
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
+ );
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is false', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: false,
+ });
+
+ expect(filter).toEqual(
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
+ });
+
+ test('it properly formats when filters passed and "showEventFilters" is true', () => {
+ const filter = getFilters({
+ filters: { created_by: 'moi', name: 'Sample' },
+ namespaceTypes: ['single', 'agnostic'],
+ showTrustedApps: false,
+ showEventFilters: true,
+ });
expect(filter).toEqual(
- '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
+ '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
});
diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts
index c9dd6ccae484c..238ae5541343c 100644
--- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts
+++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts
@@ -10,14 +10,26 @@ import { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts-
import { getGeneralFilters } from '../get_general_filters';
import { getSavedObjectTypes } from '../get_saved_object_types';
import { getTrustedAppsFilter } from '../get_trusted_apps_filter';
+import { getEventFiltersFilter } from '../get_event_filters_filter';
-export const getFilters = (
- filters: ExceptionListFilter,
- namespaceTypes: NamespaceType[],
- showTrustedApps: boolean
-): string => {
+export interface GetFiltersParams {
+ filters: ExceptionListFilter;
+ namespaceTypes: NamespaceType[];
+ showTrustedApps: boolean;
+ showEventFilters: boolean;
+}
+
+export const getFilters = ({
+ filters,
+ namespaceTypes,
+ showTrustedApps,
+ showEventFilters,
+}: GetFiltersParams): string => {
const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes });
const generalFilters = getGeneralFilters(filters, namespaces);
const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces);
- return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND ');
+ const eventFiltersFilter = getEventFiltersFilter(showEventFilters, namespaces);
+ return [generalFilters, trustedAppsFilter, eventFiltersFilter]
+ .filter((filter) => filter.trim() !== '')
+ .join(' AND ');
};
diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel
new file mode 100644
index 0000000000000..5cf1081bdd32e
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel
@@ -0,0 +1,125 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-securitysolution-t-grid"
+
+PKG_REQUIRE_NAME = "@kbn/securitysolution-t-grid"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ ],
+ exclude = [
+ "**/*.test.*",
+ "**/*.mock.*",
+ ],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "react/package.json",
+ "package.json",
+ "README.md",
+]
+
+SRC_DEPS = [
+ "//packages/kbn-babel-preset",
+ "//packages/kbn-dev-utils",
+ "//packages/kbn-i18n",
+ "@npm//@babel/core",
+ "@npm//babel-loader",
+ "@npm//enzyme",
+ "@npm//jest",
+ "@npm//lodash",
+ "@npm//react",
+ "@npm//react-beautiful-dnd",
+ "@npm//tslib",
+]
+
+TYPES_DEPS = [
+ "@npm//typescript",
+ "@npm//@types/enzyme",
+ "@npm//@types/jest",
+ "@npm//@types/lodash",
+ "@npm//@types/node",
+ "@npm//@types/react",
+ "@npm//@types/react-beautiful-dnd",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_config(
+ name = "tsconfig_browser",
+ src = "tsconfig.browser.json",
+ deps = [
+ "//:tsconfig.base.json",
+ "//:tsconfig.browser.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ["--pretty"],
+ srcs = SRCS,
+ deps = DEPS,
+ declaration = True,
+ declaration_dir = "target_types",
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target_node",
+ root_dir = "src",
+ source_map = True,
+ tsconfig = ":tsconfig",
+)
+
+ts_project(
+ name = "tsc_browser",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ allow_js = True,
+ declaration = False,
+ incremental = True,
+ out_dir = "target_web",
+ source_map = True,
+ root_dir = "src",
+ tsconfig = ":tsconfig_browser",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ package_name = PKG_REQUIRE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ visibility = ["//visibility:public"],
+ deps = [":tsc", ":tsc_browser"] + DEPS,
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ],
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-securitysolution-t-grid/README.md b/packages/kbn-securitysolution-t-grid/README.md
new file mode 100644
index 0000000000000..a49669c81689a
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/README.md
@@ -0,0 +1,3 @@
+# kbn-securitysolution-t-grid
+
+We do not want to create circular dependencies between security_solution and timelines plugins. Therefore , we will use this packages to share components between these two plugins.
diff --git a/packages/kbn-securitysolution-t-grid/babel.config.js b/packages/kbn-securitysolution-t-grid/babel.config.js
new file mode 100644
index 0000000000000..b4a118df51af5
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/babel.config.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ env: {
+ web: {
+ presets: ['@kbn/babel-preset/webpack_preset'],
+ },
+ node: {
+ presets: ['@kbn/babel-preset/node_preset'],
+ },
+ },
+ ignore: ['**/*.test.ts', '**/*.test.tsx'],
+};
diff --git a/packages/kbn-securitysolution-t-grid/jest.config.js b/packages/kbn-securitysolution-t-grid/jest.config.js
new file mode 100644
index 0000000000000..21e7d2d71b61a
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/jest.config.js
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-securitysolution-t-grid'],
+};
diff --git a/packages/kbn-securitysolution-t-grid/package.json b/packages/kbn-securitysolution-t-grid/package.json
new file mode 100644
index 0000000000000..68d3a8c71e7ca
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@kbn/securitysolution-t-grid",
+ "version": "1.0.0",
+ "description": "security solution t-grid packages will allow sharing components between timelines and security_solution plugin until we transfer all functionality to timelines plugin",
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "browser": "./target_web/browser.js",
+ "main": "./target_node/index.js",
+ "types": "./target_types/index.d.ts",
+ "private": true
+}
diff --git a/packages/kbn-securitysolution-t-grid/react/package.json b/packages/kbn-securitysolution-t-grid/react/package.json
new file mode 100644
index 0000000000000..c29ddd45f084d
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/react/package.json
@@ -0,0 +1,5 @@
+{
+ "browser": "../target_web/react",
+ "main": "../target_node/react",
+ "types": "../target_types/react/index.d.ts"
+}
\ No newline at end of file
diff --git a/packages/kbn-securitysolution-t-grid/src/constants/index.ts b/packages/kbn-securitysolution-t-grid/src/constants/index.ts
new file mode 100644
index 0000000000000..c03c0093d9839
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/constants/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const HIGHLIGHTED_DROP_TARGET_CLASS_NAME = 'highlighted-drop-target';
+export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group';
+
+/** The draggable will move this many pixels via the keyboard when the arrow key is pressed */
+export const KEYBOARD_DRAG_OFFSET = 20;
+
+export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper';
+
+export const ROW_RENDERER_CLASS_NAME = 'row-renderer';
+
+export const NOTES_CONTAINER_CLASS_NAME = 'notes-container';
+
+export const NOTE_CONTENT_CLASS_NAME = 'note-content';
+
+/** This class is added to the document body while dragging */
+export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
+
+export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show';
diff --git a/packages/kbn-securitysolution-t-grid/src/index.ts b/packages/kbn-securitysolution-t-grid/src/index.ts
new file mode 100644
index 0000000000000..0c2e9a7dbea8b
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/index.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './constants';
+export * from './utils';
+export * from './mock';
diff --git a/packages/kbn-securitysolution-t-grid/src/mock/index.ts b/packages/kbn-securitysolution-t-grid/src/mock/index.ts
new file mode 100644
index 0000000000000..dc1b63dfc33b0
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/mock/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './mock_event_details';
diff --git a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
similarity index 97%
rename from x-pack/plugins/security_solution/common/utils/mock_event_details.ts
rename to packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
index 7dc257ebb3fef..167fc9dd17a2a 100644
--- a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts
+++ b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
export const eventHit = {
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts
new file mode 100644
index 0000000000000..34e448419693b
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { has } from 'lodash/fp';
+
+export interface AppError extends Error {
+ body: {
+ message: string;
+ };
+}
+
+export interface KibanaError extends AppError {
+ body: {
+ message: string;
+ statusCode: number;
+ };
+}
+
+export interface SecurityAppError extends AppError {
+ body: {
+ message: string;
+ status_code: number;
+ };
+}
+
+export const isKibanaError = (error: unknown): error is KibanaError =>
+ has('message', error) && has('body.message', error) && has('body.statusCode', error);
+
+export const isSecurityAppError = (error: unknown): error is SecurityAppError =>
+ has('message', error) && has('body.message', error) && has('body.status_code', error);
+
+export const isAppError = (error: unknown): error is AppError =>
+ isKibanaError(error) || isSecurityAppError(error);
+
+export const isNotFoundError = (error: unknown) =>
+ (isKibanaError(error) && error.body.statusCode === 404) ||
+ (isSecurityAppError(error) && error.body.status_code === 404);
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts
new file mode 100644
index 0000000000000..91b2e88d97358
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { DropResult } from 'react-beautiful-dnd';
+
+export const draggableIdPrefix = 'draggableId';
+
+export const droppableIdPrefix = 'droppableId';
+
+export const draggableContentPrefix = `${draggableIdPrefix}.content.`;
+
+export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`;
+
+export const draggableFieldPrefix = `${draggableIdPrefix}.field.`;
+
+export const droppableContentPrefix = `${droppableIdPrefix}.content.`;
+
+export const droppableFieldPrefix = `${droppableIdPrefix}.field.`;
+
+export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`;
+
+export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`;
+
+export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`;
+
+export const getDraggableId = (dataProviderId: string): string =>
+ `${draggableContentPrefix}${dataProviderId}`;
+
+export const getDraggableFieldId = ({
+ contextId,
+ fieldId,
+}: {
+ contextId: string;
+ fieldId: string;
+}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`;
+
+export const getTimelineProviderDroppableId = ({
+ groupIndex,
+ timelineId,
+}: {
+ groupIndex: number;
+ timelineId: string;
+}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`;
+
+export const getTimelineProviderDraggableId = ({
+ dataProviderId,
+ groupIndex,
+ timelineId,
+}: {
+ dataProviderId: string;
+ groupIndex: number;
+ timelineId: string;
+}): string =>
+ `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`;
+
+export const getDroppableId = (visualizationPlaceholderId: string): string =>
+ `${droppableContentPrefix}${visualizationPlaceholderId}`;
+
+export const sourceIsContent = (result: DropResult): boolean =>
+ result.source.droppableId.startsWith(droppableContentPrefix);
+
+export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => {
+ const regex = /^droppableId\.timelineProviders\.(\S+)\./;
+ const sourceMatches = result.source.droppableId.match(regex) || [];
+ const destinationMatches =
+ (result.destination && result.destination.droppableId.match(regex)) || [];
+
+ return (
+ sourceMatches.length >= 2 &&
+ destinationMatches.length >= 2 &&
+ sourceMatches[1] === destinationMatches[1]
+ );
+};
+
+export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
+ result.draggableId.startsWith(draggableContentPrefix);
+
+export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
+ result.draggableId.startsWith(draggableFieldPrefix);
+
+export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
+
+export const destinationIsTimelineProviders = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix);
+
+export const destinationIsTimelineColumns = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix);
+
+export const destinationIsTimelineButton = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix);
+
+export const getProviderIdFromDraggable = (result: DropResult): string =>
+ result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1);
+
+export const getFieldIdFromDraggable = (result: DropResult): string =>
+ unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1));
+
+export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_');
+
+export const escapeContextId = (path: string) => path.replace(/\./g, '_');
+
+export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!');
+
+export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.');
+
+export const providerWasDroppedOnTimeline = (result: DropResult): boolean =>
+ reasonIsDrop(result) &&
+ draggableIsContent(result) &&
+ sourceIsContent(result) &&
+ destinationIsTimelineProviders(result);
+
+export const userIsReArrangingProviders = (result: DropResult): boolean =>
+ reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result);
+
+export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean =>
+ reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result);
+
+/**
+ * Prevents fields from being dragged or dropped to any area other than column
+ * header drop zone in the timeline
+ */
+export const DRAG_TYPE_FIELD = 'drag-type-field';
+
+/** This class is added to the document body while timeline field dragging */
+export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging';
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/index.ts
new file mode 100644
index 0000000000000..39629a990c539
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/index.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './api';
+export * from './drag_and_drop';
diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json
new file mode 100644
index 0000000000000..a5183ba4fd457
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig.browser.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "incremental": true,
+ "outDir": "./target_web",
+ "declaration": false,
+ "isolatedModules": true,
+ "sourceMap": true,
+ "sourceRoot": "../../../../../packages/kbn-securitysolution-t-grid/src",
+ "types": [
+ "jest",
+ "node"
+ ],
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ ],
+ "exclude": [
+ "**/__fixtures__/**/*"
+ ]
+}
diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.json b/packages/kbn-securitysolution-t-grid/tsconfig.json
new file mode 100644
index 0000000000000..8cda578edede4
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "incremental": true,
+ "outDir": "target",
+ "rootDir": "src",
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-securitysolution-t-grid/src",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "src/**/*"
+ ]
+}
diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js
index 225f93d487823..5baff607704c7 100644
--- a/packages/kbn-test/jest-preset.js
+++ b/packages/kbn-test/jest-preset.js
@@ -94,7 +94,7 @@ module.exports = {
transformIgnorePatterns: [
// ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import()
// since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842)
- '[/\\\\]node_modules(?)[/\\\\].+\\.js$',
+ '[/\\\\]node_modules(?)[/\\\\].+\\.js$',
'packages/kbn-pm/dist/index.js',
],
diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json
index 275d9fac73c58..aaff513f1591f 100644
--- a/packages/kbn-test/package.json
+++ b/packages/kbn-test/package.json
@@ -12,8 +12,5 @@
},
"kibana": {
"devOnly": true
- },
- "dependencies": {
- "@kbn/optimizer": "link:../kbn-optimizer"
}
}
\ No newline at end of file
diff --git a/packages/kbn-test/src/jest/setup/babel_polyfill.js b/packages/kbn-test/src/jest/setup/babel_polyfill.js
index d112e4d4fcb39..7dda4cceec65c 100644
--- a/packages/kbn-test/src/jest/setup/babel_polyfill.js
+++ b/packages/kbn-test/src/jest/setup/babel_polyfill.js
@@ -9,4 +9,4 @@
// Note: In theory importing the polyfill should not be needed, as Babel should
// include the necessary polyfills when using `@babel/preset-env`, but for some
// reason it did not work. See https://github.com/elastic/kibana/issues/14506
-import '@kbn/optimizer/src/node/polyfill';
+import '@kbn/optimizer/target/node/polyfill';
diff --git a/packages/kbn-tinymath/grammar/grammar.peggy b/packages/kbn-tinymath/grammar/grammar.peggy
index 1c6f8c3334c23..414bc2fa11cb7 100644
--- a/packages/kbn-tinymath/grammar/grammar.peggy
+++ b/packages/kbn-tinymath/grammar/grammar.peggy
@@ -43,7 +43,7 @@ Literal "literal"
// Quoted variables are interpreted as strings
// but unquoted variables are more restrictive
Variable
- = _ [\'] chars:(ValidChar / Space / [\"])* [\'] _ {
+ = _ '"' chars:("\\\"" { return "\""; } / [^"])* '"' _ {
return {
type: 'variable',
value: chars.join(''),
@@ -51,7 +51,7 @@ Variable
text: text()
};
}
- / _ [\"] chars:(ValidChar / Space / [\'])* [\"] _ {
+ / _ "'" chars:("\\\'" { return "\'"; } / [^'])* "'" _ {
return {
type: 'variable',
value: chars.join(''),
diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js
index bbc8503684fd4..9d87919c4f1ac 100644
--- a/packages/kbn-tinymath/test/library.test.js
+++ b/packages/kbn-tinymath/test/library.test.js
@@ -92,6 +92,7 @@ describe('Parser', () => {
expect(parse('@foo0')).toEqual(variableEqual('@foo0'));
expect(parse('.foo0')).toEqual(variableEqual('.foo0'));
expect(parse('-foo0')).toEqual(variableEqual('-foo0'));
+ expect(() => parse(`foo😀\t')`)).toThrow('Failed to parse');
});
});
@@ -103,6 +104,7 @@ describe('Parser', () => {
expect(parse('"foo bar fizz buzz"')).toEqual(variableEqual('foo bar fizz buzz'));
expect(parse('"foo bar baby"')).toEqual(variableEqual('foo bar baby'));
expect(parse(`"f'oo"`)).toEqual(variableEqual(`f'oo`));
+ expect(parse(`"foo😀\t"`)).toEqual(variableEqual(`foo😀\t`));
});
it('strings with single quotes', () => {
@@ -119,6 +121,7 @@ describe('Parser', () => {
expect(parse("'foo bar '")).toEqual(variableEqual("foo bar "));
expect(parse("'0foo'")).toEqual(variableEqual("0foo"));
expect(parse(`'f"oo'`)).toEqual(variableEqual(`f"oo`));
+ expect(parse(`'foo😀\t'`)).toEqual(variableEqual(`foo😀\t`));
/* eslint-enable prettier/prettier */
});
diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts
index 0264c8a1acf75..92f5a854f6b00 100644
--- a/src/core/public/chrome/chrome_service.test.ts
+++ b/src/core/public/chrome/chrome_service.test.ts
@@ -53,8 +53,21 @@ function defaultStartDeps(availableApps?: App[]) {
return deps;
}
+function defaultStartTestOptions({
+ browserSupportsCsp = true,
+ kibanaVersion = 'version',
+}: {
+ browserSupportsCsp?: boolean;
+ kibanaVersion?: string;
+}): any {
+ return {
+ browserSupportsCsp,
+ kibanaVersion,
+ };
+}
+
async function start({
- options = { browserSupportsCsp: true },
+ options = defaultStartTestOptions({}),
cspConfigMock = { warnLegacyBrowsers: true },
startDeps = defaultStartDeps(),
}: { options?: any; cspConfigMock?: any; startDeps?: ReturnType } = {}) {
@@ -82,7 +95,9 @@ afterAll(() => {
describe('start', () => {
it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => {
- const { startDeps } = await start({ options: { browserSupportsCsp: false } });
+ const { startDeps } = await start({
+ options: { browserSupportsCsp: false, kibanaVersion: '7.0.0' },
+ });
expect(startDeps.notifications.toasts.addWarning.mock.calls).toMatchInlineSnapshot(`
Array [
@@ -95,6 +110,41 @@ describe('start', () => {
`);
});
+ it('adds the kibana versioned class to the document body', async () => {
+ const { chrome, service } = await start({
+ options: { browserSupportsCsp: false, kibanaVersion: '1.2.3' },
+ });
+ const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise();
+ service.stop();
+ await expect(promise).resolves.toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "kbnBody",
+ "kbnBody--noHeaderBanner",
+ "kbnBody--chromeHidden",
+ "kbnVersion-1-2-3",
+ ],
+ ]
+ `);
+ });
+ it('strips off "snapshot" from the kibana version if present', async () => {
+ const { chrome, service } = await start({
+ options: { browserSupportsCsp: false, kibanaVersion: '8.0.0-SnAPshot' },
+ });
+ const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise();
+ service.stop();
+ await expect(promise).resolves.toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "kbnBody",
+ "kbnBody--noHeaderBanner",
+ "kbnBody--chromeHidden",
+ "kbnVersion-8-0-0",
+ ],
+ ]
+ `);
+ });
+
it('does not add legacy browser warning if browser supports CSP', async () => {
const { startDeps } = await start();
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index 5ed447edde75a..f1381c52ce779 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -37,9 +37,11 @@ import {
export type { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle };
const IS_LOCKED_KEY = 'core.chrome.isLocked';
+const SNAPSHOT_REGEX = /-snapshot/i;
interface ConstructorParams {
browserSupportsCsp: boolean;
+ kibanaVersion: string;
}
interface StartDeps {
@@ -116,6 +118,16 @@ export class ChromeService {
const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK);
const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true');
+ const getKbnVersionClass = () => {
+ // we assume that the version is valid and has the form 'X.X.X'
+ // strip out `SNAPSHOT` and reformat to 'X-X-X'
+ const formattedVersionClass = this.params.kibanaVersion
+ .replace(SNAPSHOT_REGEX, '')
+ .split('.')
+ .join('-');
+ return `kbnVersion-${formattedVersionClass}`;
+ };
+
const headerBanner$ = new BehaviorSubject(undefined);
const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe(
map(([headerBanner, isVisible]) => {
@@ -123,6 +135,7 @@ export class ChromeService {
'kbnBody',
headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner',
isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden',
+ getKbnVersionClass(),
];
})
);
diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
index 3668829a6888c..0b10209bc13e5 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
@@ -370,54 +370,62 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
isOpen={true}
onClose={[Function]}
>
-
-
-
- }
- />
-
-
-
-
+
-
-
-
-
+ Custom link
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+ data-euiicon-type="home"
+ />
+
+
+ Home
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ Recently viewed
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-recentlyViewed"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="recentlyViewed"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Recently viewed"
+ paddingSize="none"
>
-
-
-
-
- Recently viewed
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
- Recently viewed
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+ Analytics
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-kibana"
- iconType="logoKibana"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="kibana"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Analytics"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Analytics
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Analytics
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+ dashboard
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Observability
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-observability"
- iconType="logoObservability"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="observability"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Observability"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Observability
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Observability
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Security
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-securitySolution"
- iconType="logoSecurity"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="securitySolution"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Security"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Security
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Security
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Management
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-management"
- iconType="managementApp"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="management"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Management"
+ paddingSize="none"
>
-
-
-
-
-
-
-
- Management
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
-
- Management
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+ monitoring
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- Dock navigation
-
- ,
- }
- }
- color="subdued"
- data-test-subj="collapsible-nav-lock"
- iconType="lockOpen"
- label="Dock navigation"
- onClick={[Function]}
- size="xs"
- >
-
-
+
-
-
-
+ data-euiicon-type="lockOpen"
+ />
Dock navigation
-
-
-
-
-
-
+ ,
+ }
+ }
+ color="subdued"
+ data-test-subj="collapsible-nav-lock"
+ iconType="lockOpen"
+ label="Dock navigation"
+ onClick={[Function]}
+ size="xs"
+ >
+
+
+
+
+
+
+ Dock navigation
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
@@ -2770,42 +2706,57 @@ exports[`CollapsibleNav renders the default nav 3`] = `
isOpen={false}
onClose={[Function]}
>
-
-
-
-
-
-
-
-
+ data-euiicon-type="home"
+ />
+
+
+ Home
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ Recently viewed
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-recentlyViewed"
+ id="generated-id"
initialIsOpen={true}
- isCollapsible={true}
- key="recentlyViewed"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Recently viewed"
+ paddingSize="none"
>
-
-
-
-
- Recently viewed
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
- Recently viewed
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
- No recently viewed items
-
-
-
-
-
-
+
+ No recently viewed items
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
-
-
- Undock navigation
-
- ,
- }
- }
- color="subdued"
- data-test-subj="collapsible-nav-lock"
- iconType="lock"
- label="Undock navigation"
- onClick={[Function]}
- size="xs"
- >
-
-
-
-
-
+ data-euiicon-type="lock"
+ />
Undock navigation
-
-
-
-
-
-
+ ,
+ }
+ }
+ color="subdued"
+ data-test-subj="collapsible-nav-lock"
+ iconType="lock"
+ label="Undock navigation"
+ onClick={[Function]}
+ size="xs"
+ >
+
+
+
+
+
+
+ Undock navigation
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
index 6ad1e2d3a1cc6..5aee9ca1b7c08 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
@@ -4947,42 +4947,57 @@ exports[`Header renders 1`] = `
isOpen={false}
onClose={[Function]}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Manage cloud deployment
-
-
-
-
-
-
-
+ Manage cloud deployment
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+ data-euiicon-type="home"
+ />
+
+
+ Home
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ Recently viewed
+
+
+
+
+ }
+ className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-recentlyViewed"
+ id="mockId"
initialIsOpen={true}
- isCollapsible={true}
- key="recentlyViewed"
+ isLoading={false}
+ isLoadingMessage={false}
onToggle={[Function]}
- title="Recently viewed"
+ paddingSize="none"
>
-
-
-
-
- Recently viewed
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
- Recently viewed
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+ dashboard
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+ kibana
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Undock navigation
+
+ ,
+ }
+ }
+ color="subdued"
+ data-test-subj="collapsible-nav-lock"
+ iconType="lock"
+ label="Undock navigation"
onClick={[Function]}
- size="s"
+ size="xs"
>
+
+
+
- kibana
+ Undock navigation
@@ -5445,163 +5540,11 @@ exports[`Header renders 1`] = `
-
-
-
-
-
-
-
-
-
- Undock navigation
-
- ,
- }
- }
- color="subdued"
- data-test-subj="collapsible-nav-lock"
- iconType="lock"
- label="Undock navigation"
- onClick={[Function]}
- size="xs"
- >
-
-
-
-
-
-
- Undock navigation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
index 7f338a859e7b4..460770744d53a 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
@@ -16,10 +16,6 @@ import { httpServiceMock } from '../../../http/http_service.mock';
import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed';
import { CollapsibleNav } from './collapsible_nav';
-jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
- htmlIdGenerator: () => () => 'mockId',
-}));
-
const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES;
function mockLink({ title = 'discover', category }: Partial) {
diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx
index fdbdde8556eeb..a3a0197b4017e 100644
--- a/src/core/public/chrome/ui/header/header.test.tsx
+++ b/src/core/public/chrome/ui/header/header.test.tsx
@@ -99,7 +99,7 @@ describe('Header', () => {
act(() => isLocked$.next(true));
component.update();
- expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy();
+ expect(component.find('[data-test-subj="collapsibleNav"]').exists()).toBeTruthy();
expect(component).toMatchSnapshot();
act(() =>
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index 67cdd24aae848..246ca83ef5ade 100644
--- a/src/core/public/chrome/ui/header/header.tsx
+++ b/src/core/public/chrome/ui/header/header.tsx
@@ -87,6 +87,7 @@ export function Header({
const isVisible = useObservable(observables.isVisible$, false);
const isLocked = useObservable(observables.isLocked$, false);
const [isNavOpen, setIsNavOpen] = useState(false);
+ const [navId] = useState(htmlIdGenerator()());
const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$);
if (!isVisible) {
@@ -99,7 +100,6 @@ export function Header({
}
const toggleCollapsibleNavRef = createRef void }>();
- const navId = htmlIdGenerator()();
const className = classnames('hide-for-sharing', 'headerGlobalNav');
const Breadcrumbs = (
diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts
index 1c4e78f0a5c2e..8ead0f50785bd 100644
--- a/src/core/public/core_system.test.ts
+++ b/src/core/public/core_system.test.ts
@@ -46,6 +46,7 @@ const defaultCoreSystemParams = {
csp: {
warnLegacyBrowsers: true,
},
+ version: 'version',
} as any,
};
@@ -91,12 +92,12 @@ describe('constructor', () => {
});
});
- it('passes browserSupportsCsp to ChromeService', () => {
+ it('passes browserSupportsCsp and coreContext to ChromeService', () => {
createCoreSystem();
-
expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1);
expect(ChromeServiceConstructor).toHaveBeenCalledWith({
- browserSupportsCsp: expect.any(Boolean),
+ browserSupportsCsp: true,
+ kibanaVersion: 'version',
});
});
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index f0ea1e62fc33f..9a28bf45df927 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
import { CoreId } from '../server';
import { PackageInfo, EnvironmentMode } from '../server/types';
import { CoreSetup, CoreStart } from '.';
@@ -98,6 +97,7 @@ export class CoreSystem {
this.injectedMetadata = new InjectedMetadataService({
injectedMetadata,
});
+ this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env };
this.fatalErrors = new FatalErrorsService(rootDomElement, () => {
// Stop Core before rendering any fatal errors into the DOM
@@ -109,14 +109,16 @@ export class CoreSystem {
this.savedObjects = new SavedObjectsService();
this.uiSettings = new UiSettingsService();
this.overlay = new OverlayService();
- this.chrome = new ChromeService({ browserSupportsCsp });
+ this.chrome = new ChromeService({
+ browserSupportsCsp,
+ kibanaVersion: injectedMetadata.version,
+ });
this.docLinks = new DocLinksService();
this.rendering = new RenderingService();
this.application = new ApplicationService();
this.integrations = new IntegrationsService();
this.deprecations = new DeprecationsService();
- this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env };
this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins);
this.coreApp = new CoreApp(this.coreContext);
}
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 95091a761639b..502b22a6f8e89 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -137,6 +137,7 @@ export class DocLinksService {
addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`,
kibana: `${KIBANA_DOCS}index.html`,
upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`,
+ rollupJobs: `${KIBANA_DOCS}data-rollups.html`,
elasticsearch: {
docsBase: `${ELASTICSEARCH_DOCS}`,
asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`,
@@ -203,6 +204,7 @@ export class DocLinksService {
},
search: {
sessions: `${KIBANA_DOCS}search-sessions.html`,
+ sessionLimits: `${KIBANA_DOCS}search-sessions.html#_limitations`,
},
date: {
dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`,
@@ -522,6 +524,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -532,6 +535,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
index f5a1c51ccbe15..fbd09f3096854 100644
--- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
+++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
@@ -26,7 +26,7 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `""`;
+exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `""`;
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
Array [
@@ -59,4 +59,4 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `""`;
+exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `""`;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 6cc2b3f321fb7..ca95b253f9cdb 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -585,6 +585,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -595,6 +596,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
@@ -1630,6 +1632,6 @@ export interface UserProvidedValues {
// Warnings were encountered during analysis:
//
-// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
+// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
```
diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss
index 4bd6afe90d342..92ba28ff70887 100644
--- a/src/core/public/rendering/_base.scss
+++ b/src/core/public/rendering/_base.scss
@@ -38,6 +38,7 @@
@mixin kbnAffordForHeader($headerHeight) {
@include euiHeaderAffordForFixed($headerHeight);
+ #securitySolutionStickyKQL,
#app-fixed-viewport {
top: $headerHeight;
}
diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss
index 3386fa73f328a..de138cdf402e6 100644
--- a/src/core/public/styles/_base.scss
+++ b/src/core/public/styles/_base.scss
@@ -26,7 +26,7 @@
}
.euiBody--collapsibleNavIsDocked .euiBottomBar {
- margin-left: $euiCollapsibleNavWidth;
+ margin-left: 320px; // Hard-coded for now -- @cchaos
}
// Temporary fix for EuiPageHeader with a bottom border but no tabs or padding
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 22c40a547f419..4456784fdbc0b 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -525,15 +525,22 @@ describe('SavedObjectsRepository', () => {
const ns2 = 'bar-namespace';
const ns3 = 'baz-namespace';
const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] },
- { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] },
+ { ...obj1, type: 'dashboard', initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] },
];
await bulkCreateSuccess(objects, { namespace, overwrite: true });
const body = [
- expect.any(Object),
+ { index: expect.objectContaining({ _id: `${ns2}:dashboard:${obj1.id}` }) },
+ expect.objectContaining({ namespace: ns2 }),
+ {
+ index: expect.objectContaining({
+ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj1.id}`,
+ }),
+ },
expect.objectContaining({ namespaces: [ns2] }),
- expect.any(Object),
- expect.objectContaining({ namespaces: [ns3] }),
+ { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj1.id}` }) },
+ expect.objectContaining({ namespaces: [ns2, ns3] }),
];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
@@ -649,24 +656,19 @@ describe('SavedObjectsRepository', () => {
).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"'));
});
- it(`returns error when initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- const obj = { ...obj3, type: objType, initialNamespaces: [] };
- await bulkCreateError(
+ it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => {
+ const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestError('"initialNamespaces" can only be used on multi-namespace types')
- )
- );
- };
- await test('dashboard');
- await test(NAMESPACE_AGNOSTIC_TYPE);
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ )
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`returns error when initialNamespaces is empty`, async () => {
const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] };
await bulkCreateError(
obj,
@@ -678,6 +680,26 @@ describe('SavedObjectsRepository', () => {
);
});
+ it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ const obj = { ...obj3, type: objType, initialNamespaces };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`returns error when type is invalid`, async () => {
const obj = { ...obj3, type: 'unknownType' };
await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
@@ -1865,12 +1887,46 @@ describe('SavedObjectsRepository', () => {
});
it(`adds initialNamespaces instead of namespace`, async () => {
- const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] };
- await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options);
- expect(client.create).toHaveBeenCalledWith(
+ const ns2 = 'bar-namespace';
+ const ns3 = 'baz-namespace';
+ await savedObjectsRepository.create('dashboard', attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2, ns3],
+ });
+
+ expect(client.create).toHaveBeenCalledTimes(3);
+ expect(client.create).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ id: `${ns2}:dashboard:${id}`,
+ body: expect.objectContaining({ namespace: ns2 }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`,
+ body: expect.objectContaining({ namespaces: [ns2] }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 3,
expect.objectContaining({
id: `${MULTI_NAMESPACE_TYPE}:${id}`,
- body: expect.objectContaining({ namespaces: options.initialNamespaces }),
+ body: expect.objectContaining({ namespaces: [ns2, ns3] }),
}),
expect.anything()
);
@@ -1892,29 +1948,40 @@ describe('SavedObjectsRepository', () => {
});
describe('errors', () => {
- it(`throws when options.initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- await expect(
- savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] })
- ).rejects.toThrowError(
- createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- )
- );
- };
- await test('dashboard');
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
- await test(NAMESPACE_AGNOSTIC_TYPE);
+ it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => {
+ await expect(
+ savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, {
+ initialNamespaces: [namespace],
+ })
+ ).rejects.toThrowError(
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`throws when options.initialNamespaces is empty`, async () => {
await expect(
savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] })
).rejects.toThrowError(
- createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings')
+ createBadRequestError('"initialNamespaces" must be a non-empty array of strings')
);
});
+ it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ await expect(
+ savedObjectsRepository.create(objType, attributes, { initialNamespaces })
+ ).rejects.toThrowError(
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`throws when options.namespace is '*'`, async () => {
await expect(
savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING })
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 1577f773434b9..c9fa50da55df1 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -283,28 +283,18 @@ export class SavedObjectsRepository {
} = options;
const namespace = normalizeNamespace(options.namespace);
- if (initialNamespaces) {
- if (!this._registry.isShareable(type)) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!initialNamespaces.length) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" must be a non-empty array of strings'
- );
- }
- }
+ this.validateInitialNamespaces(type, initialNamespaces);
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
}
const time = this._getCurrentTime();
- let savedObjectNamespace;
+ let savedObjectNamespace: string | undefined;
let savedObjectNamespaces: string[] | undefined;
- if (this._registry.isSingleNamespace(type) && namespace) {
- savedObjectNamespace = namespace;
+ if (this._registry.isSingleNamespace(type)) {
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(type)) {
if (id && overwrite) {
// we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces
@@ -369,32 +359,29 @@ export class SavedObjectsRepository {
let bulkGetRequestIndexCounter = 0;
const expectedResults: Either[] = objects.map((object) => {
+ const { type, id, initialNamespaces } = object;
let error: DecoratedError | undefined;
- if (!this._allowedTypes.includes(object.type)) {
- error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type);
- } else if (object.initialNamespaces) {
- if (!this._registry.isShareable(object.type)) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!object.initialNamespaces.length) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" must be a non-empty array of strings'
- );
+ if (!this._allowedTypes.includes(type)) {
+ error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
+ } else {
+ try {
+ this.validateInitialNamespaces(type, initialNamespaces);
+ } catch (e) {
+ error = e;
}
}
if (error) {
return {
tag: 'Left' as 'Left',
- error: { id: object.id, type: object.type, error: errorContent(error) },
+ error: { id, type, error: errorContent(error) },
};
}
- const method = object.id && overwrite ? 'index' : 'create';
- const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type);
+ const method = id && overwrite ? 'index' : 'create';
+ const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type);
- if (object.id == null) {
+ if (id == null) {
object.id = SavedObjectsUtils.generateId();
}
@@ -434,8 +421,8 @@ export class SavedObjectsRepository {
return expectedBulkGetResult;
}
- let savedObjectNamespace;
- let savedObjectNamespaces;
+ let savedObjectNamespace: string | undefined;
+ let savedObjectNamespaces: string[] | undefined;
let versionProperties;
const {
esRequestIndex,
@@ -469,7 +456,7 @@ export class SavedObjectsRepository {
versionProperties = getExpectedVersionProperties(version, actualResult);
} else {
if (this._registry.isSingleNamespace(object.type)) {
- savedObjectNamespace = namespace;
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(object.type)) {
savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace);
}
@@ -2080,6 +2067,29 @@ export class SavedObjectsRepository {
const object = await this.get(type, id, options);
return { saved_object: object, outcome: 'exactMatch' };
}
+
+ private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) {
+ if (!initialNamespaces) {
+ return;
+ }
+
+ if (this._registry.isNamespaceAgnostic(type)) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" cannot be used on space-agnostic types'
+ );
+ } else if (!initialNamespaces.length) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" must be a non-empty array of strings'
+ );
+ } else if (
+ !this._registry.isShareable(type) &&
+ (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING))
+ ) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ );
+ }
+ }
}
/**
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index af682cfb81296..1423050145695 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -63,7 +63,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
@@ -96,7 +100,11 @@ export interface SavedObjectsBulkCreateObject {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9e7721fde90e7..fcecf39f7e53a 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2901,7 +2901,7 @@ export class SavedObjectsRepository {
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise;
-}
+ }
// @public
export interface SavedObjectsRepositoryFactory {
diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts
index 534d7df9d9466..e1986c5bf1d92 100644
--- a/src/core/server/server.test.ts
+++ b/src/core/server/server.test.ts
@@ -114,6 +114,7 @@ test('runs services on "start"', async () => {
expect(mockSavedObjectsService.start).not.toHaveBeenCalled();
expect(mockUiSettingsService.start).not.toHaveBeenCalled();
expect(mockMetricsService.start).not.toHaveBeenCalled();
+ expect(mockStatusService.start).not.toHaveBeenCalled();
await server.start();
@@ -121,6 +122,7 @@ test('runs services on "start"', async () => {
expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1);
expect(mockMetricsService.start).toHaveBeenCalledTimes(1);
+ expect(mockStatusService.start).toHaveBeenCalledTimes(1);
});
test('does not fail on "setup" if there are unused paths detected', async () => {
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index adf794c390338..3f553dd90678e 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -248,6 +248,7 @@ export class Server {
savedObjects: savedObjectsStart,
exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(),
});
+ this.status.start();
this.coreStart = {
capabilities: capabilitiesStart,
@@ -261,7 +262,6 @@ export class Server {
await this.plugins.start(this.coreStart);
- this.status.start();
await this.http.start();
startTransaction?.end();
diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts
index b0d9e47876940..9dc1ddcddca3e 100644
--- a/src/core/server/status/plugins_status.test.ts
+++ b/src/core/server/status/plugins_status.test.ts
@@ -8,7 +8,7 @@
import { PluginName } from '../plugins';
import { PluginsStatusService } from './plugins_status';
-import { of, Observable, BehaviorSubject } from 'rxjs';
+import { of, Observable, BehaviorSubject, ReplaySubject } from 'rxjs';
import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types';
import { first } from 'rxjs/operators';
import { ServiceStatusLevelSnapshotSerializer } from './test_utils';
@@ -34,6 +34,28 @@ describe('PluginStatusService', () => {
['c', ['a', 'b']],
]);
+ describe('set', () => {
+ it('throws an exception if called after registrations are blocked', () => {
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies,
+ });
+
+ service.blockNewRegistrations();
+ expect(() => {
+ service.set(
+ 'a',
+ of({
+ level: ServiceStatusLevels.available,
+ summary: 'fail!',
+ })
+ );
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Custom statuses cannot be registered after setup, plugin [a] attempted"`
+ );
+ });
+ });
+
describe('getDerivedStatus$', () => {
it(`defaults to core's most severe status`, async () => {
const serviceAvailable = new PluginsStatusService({
@@ -231,6 +253,75 @@ describe('PluginStatusService', () => {
{ a: { level: ServiceStatusLevels.available, summary: 'a available' } },
]);
});
+
+ it('updates when a plugin status observable emits', async () => {
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies: new Map([['a', []]]),
+ });
+ const statusUpdates: Array> = [];
+ const subscription = service
+ .getAll$()
+ .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses));
+
+ const aStatus$ = new BehaviorSubject({
+ level: ServiceStatusLevels.degraded,
+ summary: 'a degraded',
+ });
+ service.set('a', aStatus$);
+ aStatus$.next({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' });
+ aStatus$.next({ level: ServiceStatusLevels.available, summary: 'a available' });
+ subscription.unsubscribe();
+
+ expect(statusUpdates).toEqual([
+ { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } },
+ { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } },
+ { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } },
+ { a: { level: ServiceStatusLevels.available, summary: 'a available' } },
+ ]);
+ });
+
+ it('emits an unavailable status if first emission times out, then continues future emissions', async () => {
+ jest.useFakeTimers();
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies: new Map([
+ ['a', []],
+ ['b', ['a']],
+ ]),
+ });
+
+ const pluginA$ = new ReplaySubject(1);
+ service.set('a', pluginA$);
+ const firstEmission = service.getAll$().pipe(first()).toPromise();
+ jest.runAllTimers();
+
+ expect(await firstEmission).toEqual({
+ a: { level: ServiceStatusLevels.unavailable, summary: 'Status check timed out after 30s' },
+ b: {
+ level: ServiceStatusLevels.unavailable,
+ summary: '[a]: Status check timed out after 30s',
+ detail: 'See the status page for more information',
+ meta: {
+ affectedServices: {
+ a: {
+ level: ServiceStatusLevels.unavailable,
+ summary: 'Status check timed out after 30s',
+ },
+ },
+ },
+ },
+ });
+
+ pluginA$.next({ level: ServiceStatusLevels.available, summary: 'a available' });
+ const secondEmission = service.getAll$().pipe(first()).toPromise();
+ jest.runAllTimers();
+ expect(await secondEmission).toEqual({
+ a: { level: ServiceStatusLevels.available, summary: 'a available' },
+ b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
+ });
+ jest.useRealTimers();
+ });
});
describe('getDependenciesStatus$', () => {
diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts
index 1aacbf3be56db..6a8ef1081e165 100644
--- a/src/core/server/status/plugins_status.ts
+++ b/src/core/server/status/plugins_status.ts
@@ -7,13 +7,22 @@
*/
import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs';
-import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators';
+import {
+ map,
+ distinctUntilChanged,
+ switchMap,
+ debounceTime,
+ timeoutWith,
+ startWith,
+} from 'rxjs/operators';
import { isDeepStrictEqual } from 'util';
import { PluginName } from '../plugins';
-import { ServiceStatus, CoreStatus } from './types';
+import { ServiceStatus, CoreStatus, ServiceStatusLevels } from './types';
import { getSummaryStatus } from './get_summary_status';
+const STATUS_TIMEOUT_MS = 30 * 1000; // 30 seconds
+
interface Deps {
core$: Observable;
pluginDependencies: ReadonlyMap;
@@ -23,6 +32,7 @@ export class PluginsStatusService {
private readonly pluginStatuses = new Map>();
private readonly update$ = new BehaviorSubject(true);
private readonly defaultInheritedStatus$: Observable;
+ private newRegistrationsAllowed = true;
constructor(private readonly deps: Deps) {
this.defaultInheritedStatus$ = this.deps.core$.pipe(
@@ -35,10 +45,19 @@ export class PluginsStatusService {
}
public set(plugin: PluginName, status$: Observable) {
+ if (!this.newRegistrationsAllowed) {
+ throw new Error(
+ `Custom statuses cannot be registered after setup, plugin [${plugin}] attempted`
+ );
+ }
this.pluginStatuses.set(plugin, status$);
this.update$.next(true); // trigger all existing Observables to update from the new source Observable
}
+ public blockNewRegistrations() {
+ this.newRegistrationsAllowed = false;
+ }
+
public getAll$(): Observable> {
return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]);
}
@@ -86,13 +105,22 @@ export class PluginsStatusService {
return this.update$.pipe(
switchMap(() => {
const pluginStatuses = plugins
- .map(
- (depName) =>
- [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [
- PluginName,
- Observable
- ]
- )
+ .map((depName) => {
+ const pluginStatus = this.pluginStatuses.get(depName)
+ ? this.pluginStatuses.get(depName)!.pipe(
+ timeoutWith(
+ STATUS_TIMEOUT_MS,
+ this.pluginStatuses.get(depName)!.pipe(
+ startWith({
+ level: ServiceStatusLevels.unavailable,
+ summary: `Status check timed out after ${STATUS_TIMEOUT_MS / 1000}s`,
+ })
+ )
+ )
+ )
+ : this.getDerivedStatus$(depName);
+ return [depName, pluginStatus] as [PluginName, Observable];
+ })
.map(([pName, status$]) =>
status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus]))
);
diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts
index b8c19508a5d61..d4dc8ed3d4d72 100644
--- a/src/core/server/status/status_service.ts
+++ b/src/core/server/status/status_service.ts
@@ -135,9 +135,11 @@ export class StatusService implements CoreService {
}
public start() {
- if (!this.overall$) {
- throw new Error('cannot call `start` before `setup`');
+ if (!this.pluginsStatus || !this.overall$) {
+ throw new Error(`StatusService#setup must be called before #start`);
}
+ this.pluginsStatus.blockNewRegistrations();
+
getOverallStatusChanges(this.overall$, this.stop$).subscribe((message) => {
this.logger.info(message);
});
diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts
index 411b942c8eb33..bfca4c74d9365 100644
--- a/src/core/server/status/types.ts
+++ b/src/core/server/status/types.ts
@@ -196,6 +196,9 @@ export interface StatusServiceSetup {
* Completely overrides the default inherited status.
*
* @remarks
+ * The first emission from this Observable should occur within 30s, else this plugin's status will fallback to
+ * `unavailable` until the first emission.
+ *
* See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status
* calculation that is provided by Core.
*/
diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat
new file mode 100755
index 0000000000000..9221af3142e61
--- /dev/null
+++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat
@@ -0,0 +1,35 @@
+@echo off
+
+SETLOCAL ENABLEDELAYEDEXPANSION
+
+set SCRIPT_DIR=%~dp0
+for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
+
+set NODE=%DIR%\node\node.exe
+
+If Not Exist "%NODE%" (
+ Echo unable to find usable node.js executable.
+ Exit /B 1
+)
+
+set CONFIG_DIR=%KBN_PATH_CONF%
+If [%KBN_PATH_CONF%] == [] (
+ set "CONFIG_DIR=%DIR%\config"
+)
+
+IF EXIST "%CONFIG_DIR%\node.options" (
+ for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do (
+ If [!NODE_OPTIONS!] == [] (
+ set "NODE_OPTIONS=%%i"
+ ) Else (
+ set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
+ )
+ )
+)
+
+TITLE Kibana Encryption Keys
+"%NODE%" "%DIR%\src\cli_encryption_keys\dist" %*
+
+:finally
+
+ENDLOCAL
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index a9b2dd6aefdda..d109a824ca81d 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -69,7 +69,6 @@ kibana_vars=(
logging.appenders
logging.appenders.console
logging.appenders.file
- logging.appenders.rolling-file
logging.dest
logging.json
logging.loggers
@@ -204,8 +203,8 @@ kibana_vars=(
xpack.actions.proxyUrl
xpack.actions.rejectUnauthorized
xpack.actions.responseTimeout
- xpack.actions.tls.proxyVerificationMode
- xpack.actions.tls.verificationMode
+ xpack.actions.ssl.proxyVerificationMode
+ xpack.actions.ssl.verificationMode
xpack.alerting.healthCheck.interval
xpack.alerting.invalidateApiKeysTask.interval
xpack.alerting.invalidateApiKeysTask.removalDelay
diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts
index 050743114f657..2c54bb8dba179 100644
--- a/src/dev/typescript/projects.ts
+++ b/src/dev/typescript/projects.ts
@@ -22,6 +22,9 @@ export const PROJECTS = [
new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), {
name: 'security_solution/cypress',
}),
+ new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), {
+ name: 'osquery/cypress',
+ }),
new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), {
name: 'apm/cypress',
disableTypeCheck: true,
@@ -55,6 +58,9 @@ export const PROJECTS = [
...glob
.sync('test/interpreter_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT })
.map((path) => new Project(resolve(REPO_ROOT, path))),
+ ...glob
+ .sync('test/server_integration/__fixtures__/plugins/*/tsconfig.json', { cwd: REPO_ROOT })
+ .map((path) => new Project(resolve(REPO_ROOT, path))),
];
export function filterProjectsByFlag(projectFlag?: string) {
diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx
index eb746e313d228..8514d41c04a51 100644
--- a/src/plugins/console/public/application/components/welcome_panel.tsx
+++ b/src/plugins/console/public/application/components/welcome_panel.tsx
@@ -27,7 +27,7 @@ interface Props {
export function WelcomePanel(props: Props) {
return (
-
+
diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
index 9f56740fdac22..afe339f3f43a2 100644
--- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
+++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
@@ -603,7 +603,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = `
}
>
-
-
+
@@ -950,7 +950,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = `
}
>
-
-
+
diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts
index 45724796c3518..d7b3c630d1a6e 100644
--- a/src/plugins/data/common/es_query/es_query/build_es_query.ts
+++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts
@@ -10,9 +10,9 @@ import { groupBy, has, isEqual } from 'lodash';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';
-import { IIndexPattern } from '../../index_patterns';
import { Filter } from '../filters';
import { Query } from '../../query/types';
+import { IndexPatternBase } from './types';
export interface EsQueryConfig {
allowLeadingWildcards: boolean;
@@ -36,7 +36,7 @@ function removeMatchAll(filters: T[]) {
* config contains dateformat:tz
*/
export function buildEsQuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queries: Query | Query[],
filters: Filter | Filter[],
config: EsQueryConfig = {
diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
index 478263d5ce601..b376436756092 100644
--- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
+++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
@@ -6,15 +6,16 @@
* Side Public License, v 1.
*/
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
import { Filter } from '../filters';
+import { IndexPatternBase } from './types';
/*
* TODO: We should base this on something better than `filter.meta.key`. We should probably modify
* this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking
* change.
*/
-export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) {
+export function filterMatchesIndex(filter: Filter, indexPattern?: IndexPatternBase | null) {
if (!filter.meta?.key || !indexPattern) {
return true;
}
diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts
index e50862235af1d..7b3c58d45a569 100644
--- a/src/plugins/data/common/es_query/es_query/from_filters.ts
+++ b/src/plugins/data/common/es_query/es_query/from_filters.ts
@@ -10,7 +10,7 @@ import { isUndefined } from 'lodash';
import { migrateFilter } from './migrate_filter';
import { filterMatchesIndex } from './filter_matches_index';
import { Filter, cleanFilter, isFilterDisabled } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
import { handleNestedFilter } from './handle_nested_filter';
/**
@@ -45,7 +45,7 @@ const translateToQuery = (filter: Filter) => {
export const buildQueryFromFilters = (
filters: Filter[] = [],
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
ignoreFilterIfFieldNotInIndex: boolean = false
) => {
filters = filters.filter((filter) => filter && !isFilterDisabled(filter));
diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts
index afedaae45872b..3eccfd8776113 100644
--- a/src/plugins/data/common/es_query/es_query/from_kuery.ts
+++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts
@@ -7,11 +7,11 @@
*/
import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
import { Query } from '../../query/types';
export function buildQueryFromKuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queries: Query[] = [],
allowLeadingWildcards: boolean = false,
dateFormatTZ?: string
@@ -24,7 +24,7 @@ export function buildQueryFromKuery(
}
function buildQuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queryASTs: KueryNode[],
config: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
index ee5305132042a..d312d034df564 100644
--- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
+++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
@@ -9,13 +9,14 @@
import { handleNestedFilter } from './handle_nested_filter';
import { fields } from '../../index_patterns/mocks';
import { buildPhraseFilter, buildQueryFilter } from '../filters';
-import { IFieldType, IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
+import { IFieldType } from '../../index_patterns';
describe('handleNestedFilter', function () {
- const indexPattern: IIndexPattern = ({
+ const indexPattern: IndexPatternBase = {
id: 'logstash-*',
fields,
- } as unknown) as IIndexPattern;
+ };
it("should return the filter's query wrapped in nested query if the target field is nested", () => {
const field = getField('nestedField.child');
diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
index 93927d81565ef..60e92769503fb 100644
--- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
+++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
@@ -7,9 +7,9 @@
*/
import { getFilterField, cleanFilter, Filter } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
-export const handleNestedFilter = (filter: Filter, indexPattern?: IIndexPattern) => {
+export const handleNestedFilter = (filter: Filter, indexPattern?: IndexPatternBase) => {
if (!indexPattern) return filter;
const fieldName = getFilterField(filter);
diff --git a/src/plugins/data/common/es_query/es_query/index.ts b/src/plugins/data/common/es_query/es_query/index.ts
index 31529480c8ac9..c10ea5846ae3f 100644
--- a/src/plugins/data/common/es_query/es_query/index.ts
+++ b/src/plugins/data/common/es_query/es_query/index.ts
@@ -11,3 +11,4 @@ export { buildQueryFromFilters } from './from_filters';
export { luceneStringToDsl } from './lucene_string_to_dsl';
export { decorateQuery } from './decorate_query';
export { getEsQueryConfig } from './get_es_query_config';
+export { IndexPatternBase } from './types';
diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts
index c7c44d019a31c..9bd78b092fc18 100644
--- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts
+++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts
@@ -9,7 +9,7 @@
import { get, omit } from 'lodash';
import { getConvertedValueForField } from '../filters';
import { Filter } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
export interface DeprecatedMatchPhraseFilter extends Filter {
query: {
@@ -28,7 +28,7 @@ function isDeprecatedMatchPhraseFilter(filter: any): filter is DeprecatedMatchPh
return Boolean(fieldName && get(filter, ['query', 'match', fieldName, 'type']) === 'phrase');
}
-export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) {
+export function migrateFilter(filter: Filter, indexPattern?: IndexPatternBase) {
if (isDeprecatedMatchPhraseFilter(filter)) {
const fieldName = Object.keys(filter.query.match)[0];
const params: Record = get(filter, ['query', 'match', fieldName]);
diff --git a/src/plugins/data/common/es_query/es_query/types.ts b/src/plugins/data/common/es_query/es_query/types.ts
new file mode 100644
index 0000000000000..2133736516049
--- /dev/null
+++ b/src/plugins/data/common/es_query/es_query/types.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { IFieldType } from '../../index_patterns';
+
+export interface IndexPatternBase {
+ fields: IFieldType[];
+ id?: string;
+}
diff --git a/src/plugins/data/common/es_query/filters/build_filters.ts b/src/plugins/data/common/es_query/filters/build_filters.ts
index ba1bd0a615493..369f9530fb92b 100644
--- a/src/plugins/data/common/es_query/filters/build_filters.ts
+++ b/src/plugins/data/common/es_query/filters/build_filters.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { IIndexPattern, IFieldType } from '../..';
+import { IFieldType, IndexPatternBase } from '../..';
import {
Filter,
FILTERS,
@@ -19,7 +19,7 @@ import {
} from '.';
export function buildFilter(
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
field: IFieldType,
type: FILTERS,
negate: boolean,
@@ -59,7 +59,7 @@ export function buildCustomFilter(
}
function buildBaseFilter(
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
field: IFieldType,
type: FILTERS,
params: any
diff --git a/src/plugins/data/common/es_query/filters/exists_filter.ts b/src/plugins/data/common/es_query/filters/exists_filter.ts
index 441a6bcb924b7..4836950c3bb27 100644
--- a/src/plugins/data/common/es_query/filters/exists_filter.ts
+++ b/src/plugins/data/common/es_query/filters/exists_filter.ts
@@ -7,7 +7,8 @@
*/
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
export type ExistsFilterMeta = FilterMeta;
@@ -26,7 +27,7 @@ export const getExistsFilterField = (filter: ExistsFilter) => {
return filter.exists && filter.exists.field;
};
-export const buildExistsFilter = (field: IFieldType, indexPattern: IIndexPattern) => {
+export const buildExistsFilter = (field: IFieldType, indexPattern: IndexPatternBase) => {
return {
meta: {
index: indexPattern.id,
diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts
index 133f5cd232e6f..fe7cdadabaee3 100644
--- a/src/plugins/data/common/es_query/filters/index.ts
+++ b/src/plugins/data/common/es_query/filters/index.ts
@@ -14,10 +14,8 @@ export * from './custom_filter';
export * from './exists_filter';
export * from './geo_bounding_box_filter';
export * from './geo_polygon_filter';
-export * from './get_display_value';
export * from './get_filter_field';
export * from './get_filter_params';
-export * from './get_index_pattern_from_filter';
export * from './match_all_filter';
export * from './meta_filter';
export * from './missing_filter';
diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/src/plugins/data/common/es_query/filters/phrase_filter.ts
index 85562435e68d0..27c1e85562097 100644
--- a/src/plugins/data/common/es_query/filters/phrase_filter.ts
+++ b/src/plugins/data/common/es_query/filters/phrase_filter.ts
@@ -8,7 +8,8 @@
import type { estypes } from '@elastic/elasticsearch';
import { get, isPlainObject } from 'lodash';
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
export type PhraseFilterMeta = FilterMeta & {
params?: {
@@ -60,7 +61,7 @@ export const getPhraseFilterValue = (filter: PhraseFilter): PhraseFilterValue =>
export const buildPhraseFilter = (
field: IFieldType,
value: any,
- indexPattern: IIndexPattern
+ indexPattern: IndexPatternBase
): PhraseFilter => {
const convertedValue = getConvertedValueForField(field, value);
diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts
index 849c1b3faef2a..8a79472154493 100644
--- a/src/plugins/data/common/es_query/filters/phrases_filter.ts
+++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts
@@ -9,7 +9,8 @@
import { Filter, FilterMeta } from './meta_filter';
import { getPhraseScript } from './phrase_filter';
import { FILTERS } from './index';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '../es_query';
export type PhrasesFilterMeta = FilterMeta & {
params: string[]; // The unformatted values
@@ -34,7 +35,7 @@ export const getPhrasesFilterField = (filter: PhrasesFilter) => {
export const buildPhrasesFilter = (
field: IFieldType,
params: any[],
- indexPattern: IIndexPattern
+ indexPattern: IndexPatternBase
) => {
const index = indexPattern.id;
const type = FILTERS.PHRASES;
diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts
index a082b93c0a79a..7bc7a8cff7487 100644
--- a/src/plugins/data/common/es_query/filters/range_filter.ts
+++ b/src/plugins/data/common/es_query/filters/range_filter.ts
@@ -8,7 +8,8 @@
import type { estypes } from '@elastic/elasticsearch';
import { map, reduce, mapValues, get, keys, pickBy } from 'lodash';
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
const OPERANDS_IN_RANGE = 2;
@@ -93,7 +94,7 @@ const format = (field: IFieldType, value: any) =>
export const buildRangeFilter = (
field: IFieldType,
params: RangeFilterParams,
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
formattedValue?: string
): RangeFilter => {
const filter: any = { meta: { index: indexPattern.id, params: {} } };
diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/src/plugins/data/common/es_query/kuery/ast/ast.ts
index be82128969968..3e7b25897cab7 100644
--- a/src/plugins/data/common/es_query/kuery/ast/ast.ts
+++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts
@@ -10,10 +10,10 @@ import { JsonObject } from '@kbn/common-utils';
import { nodeTypes } from '../node_types/index';
import { KQLSyntaxError } from '../kuery_syntax_error';
import { KueryNode, DslQuery, KueryParseOptions } from '../types';
-import { IIndexPattern } from '../../../index_patterns/types';
// @ts-ignore
import { parse as parseKuery } from './_generated_/kuery';
+import { IndexPatternBase } from '../..';
const fromExpression = (
expression: string | DslQuery,
@@ -65,7 +65,7 @@ export const fromKueryExpression = (
*/
export const toElasticsearchQuery = (
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
): JsonObject => {
diff --git a/src/plugins/data/common/es_query/kuery/functions/and.ts b/src/plugins/data/common/es_query/kuery/functions/and.ts
index 1989704cb627e..ba7d5d1f6645b 100644
--- a/src/plugins/data/common/es_query/kuery/functions/and.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/and.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(children: KueryNode[]) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/exists.ts b/src/plugins/data/common/es_query/kuery/functions/exists.ts
index 5238fb1d8ee7f..fa6c37e6ba18f 100644
--- a/src/plugins/data/common/es_query/kuery/functions/exists.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/exists.ts
@@ -8,7 +8,7 @@
import { get } from 'lodash';
import * as literal from '../node_types/literal';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { KueryNode, IFieldType, IndexPatternBase } from '../../..';
export function buildNodeParams(fieldName: string) {
return {
@@ -18,7 +18,7 @@ export function buildNodeParams(fieldName: string) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
index f2498f3ea2ad4..38a433b1b80ab 100644
--- a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
@@ -9,7 +9,7 @@
import _ from 'lodash';
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
-import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..';
export function buildNodeParams(fieldName: string, params: any) {
params = _.pick(params, 'topLeft', 'bottomRight');
@@ -26,7 +26,7 @@ export function buildNodeParams(fieldName: string, params: any) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
index 584a315930d9c..69de7248a7b38 100644
--- a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
@@ -8,7 +8,7 @@
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
-import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..';
import { LiteralTypeBuildNode } from '../node_types/types';
export function buildNodeParams(fieldName: string, points: LatLon[]) {
@@ -25,7 +25,7 @@ export function buildNodeParams(fieldName: string, points: LatLon[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/is.ts b/src/plugins/data/common/es_query/kuery/functions/is.ts
index a18ad230c3cae..55d036c2156f9 100644
--- a/src/plugins/data/common/es_query/kuery/functions/is.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/is.ts
@@ -11,7 +11,7 @@ import { getPhraseScript } from '../../filters';
import { getFields } from './utils/get_fields';
import { getTimeZoneFromSettings } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType } from '../../..';
import * as ast from '../ast';
@@ -39,7 +39,7 @@ export function buildNodeParams(fieldName: string, value: any, isPhrase: boolean
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/nested.ts b/src/plugins/data/common/es_query/kuery/functions/nested.ts
index bfd01ef39764c..46ceeaf3e5de6 100644
--- a/src/plugins/data/common/es_query/kuery/functions/nested.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/nested.ts
@@ -8,7 +8,7 @@
import * as ast from '../ast';
import * as literal from '../node_types/literal';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(path: any, child: any) {
const pathNode =
@@ -20,7 +20,7 @@ export function buildNodeParams(path: any, child: any) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/not.ts b/src/plugins/data/common/es_query/kuery/functions/not.ts
index ef4456897bcdd..f837cd261c814 100644
--- a/src/plugins/data/common/es_query/kuery/functions/not.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/not.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(child: KueryNode) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(child: KueryNode) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/or.ts b/src/plugins/data/common/es_query/kuery/functions/or.ts
index 416687e7cde9c..7365cc39595e6 100644
--- a/src/plugins/data/common/es_query/kuery/functions/or.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/or.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(children: KueryNode[]) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/range.ts b/src/plugins/data/common/es_query/kuery/functions/range.ts
index 06b345e5821c3..caefa7e5373ca 100644
--- a/src/plugins/data/common/es_query/kuery/functions/range.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/range.ts
@@ -13,7 +13,7 @@ import { getRangeScript, RangeFilterParams } from '../../filters';
import { getFields } from './utils/get_fields';
import { getTimeZoneFromSettings } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType } from '../../..';
export function buildNodeParams(fieldName: string, params: RangeFilterParams) {
const paramsToMap = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format');
@@ -33,7 +33,7 @@ export function buildNodeParams(fieldName: string, params: RangeFilterParams) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
index 4002a36648f04..7dac1262d5062 100644
--- a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
@@ -8,10 +8,10 @@
import * as literal from '../../node_types/literal';
import * as wildcard from '../../node_types/wildcard';
-import { KueryNode, IIndexPattern } from '../../../..';
+import { KueryNode, IndexPatternBase } from '../../../..';
import { LiteralTypeBuildNode } from '../../node_types/types';
-export function getFields(node: KueryNode, indexPattern?: IIndexPattern) {
+export function getFields(node: KueryNode, indexPattern?: IndexPatternBase) {
if (!indexPattern) return [];
if (node.type === 'literal') {
const fieldName = literal.toElasticsearchQuery(node as LiteralTypeBuildNode);
diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
index e623579226861..644791637aa70 100644
--- a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
@@ -7,11 +7,11 @@
*/
import { getFields } from './get_fields';
-import { IIndexPattern, IFieldType, KueryNode } from '../../../..';
+import { IndexPatternBase, IFieldType, KueryNode } from '../../../..';
export function getFullFieldNameNode(
rootNameNode: any,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
nestedPath?: string
): KueryNode {
const fullFieldNameNode = {
diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.ts b/src/plugins/data/common/es_query/kuery/node_types/function.ts
index b9b7379dfb23d..642089a101f31 100644
--- a/src/plugins/data/common/es_query/kuery/node_types/function.ts
+++ b/src/plugins/data/common/es_query/kuery/node_types/function.ts
@@ -9,7 +9,7 @@
import _ from 'lodash';
import { functions } from '../functions';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
import { FunctionName, FunctionTypeBuildNode } from './types';
export function buildNode(functionName: FunctionName, ...args: any[]) {
@@ -45,7 +45,7 @@ export function buildNodeWithArgumentNodes(
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
) {
diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/src/plugins/data/common/es_query/kuery/node_types/types.ts
index b3247a0ad8dc2..ea8eb5e8a0618 100644
--- a/src/plugins/data/common/es_query/kuery/node_types/types.ts
+++ b/src/plugins/data/common/es_query/kuery/node_types/types.ts
@@ -11,8 +11,8 @@
*/
import { JsonValue } from '@kbn/common-utils';
-import { IIndexPattern } from '../../../index_patterns';
import { KueryNode } from '..';
+import { IndexPatternBase } from '../..';
export type FunctionName =
| 'is'
@@ -30,7 +30,7 @@ interface FunctionType {
buildNodeWithArgumentNodes: (functionName: FunctionName, args: any[]) => FunctionTypeBuildNode;
toElasticsearchQuery: (
node: any,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
) => JsonValue;
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index 07aa8967b905e..a88f029c0c7cd 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -9,6 +9,7 @@ import type { estypes } from '@elastic/elasticsearch';
import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications';
// eslint-disable-next-line
import type { SavedObject } from 'src/core/server';
+import type { IndexPatternBase } from '../es_query';
import { IFieldType } from './fields';
import { RUNTIME_FIELD_TYPES } from './constants';
import { SerializedFieldFormat } from '../../../expressions/common';
@@ -29,10 +30,8 @@ export interface RuntimeField {
* IIndexPattern allows for an IndexPattern OR an index pattern saved object
* Use IndexPattern or IndexPatternSpec instead
*/
-export interface IIndexPattern {
- fields: IFieldType[];
+export interface IIndexPattern extends IndexPatternBase {
title: string;
- id?: string;
/**
* Type is used for identifying rollup indices, otherwise left undefined
*/
diff --git a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
index 4d8ee0f889173..91379ea054de3 100644
--- a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
+++ b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
@@ -20,7 +20,7 @@ export const parseTimeShift = (val: string): moment.Duration | 'previous' | 'inv
if (trimmedVal === 'previous') {
return 'previous';
}
- const [, amount, unit] = trimmedVal.match(/^(\d+)(\w)$/) || [];
+ const [, amount, unit] = trimmedVal.match(/^(\d+)\s*(\w)$/) || [];
const parsedAmount = Number(amount);
if (Number.isNaN(parsedAmount) || !allowedUnits.includes(unit as AllowedUnit)) {
return 'invalid';
diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts
index d1890ec97df4e..c5cf3f9f09e6c 100644
--- a/src/plugins/data/common/search/types.ts
+++ b/src/plugins/data/common/search/types.ts
@@ -65,6 +65,11 @@ export interface IKibanaSearchResponse {
*/
isPartial?: boolean;
+ /**
+ * Indicates whether the results returned are from the async-search index
+ */
+ isRestored?: boolean;
+
/**
* The raw response returned by the internal search method (usually the raw ES response)
*/
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index 078dd3a9b7c5a..d7667f20d517e 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -23,7 +23,6 @@ import {
disableFilter,
FILTERS,
FilterStateStore,
- getDisplayValueFromFilter,
getPhraseFilterField,
getPhraseFilterValue,
isExistsFilter,
@@ -43,6 +42,7 @@ import { FilterLabel } from './ui';
import { FilterItem } from './ui/filter_bar';
import {
+ getDisplayValueFromFilter,
generateFilters,
onlyDisabledFiltersChanged,
changeTimeFilter,
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 4d9c69b137a3e..2849b93b14483 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -808,11 +808,11 @@ export const esFilters: {
FILTERS: typeof FILTERS;
FilterStateStore: typeof FilterStateStore;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter;
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter;
isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter;
isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter;
isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter;
@@ -858,7 +858,7 @@ export const esFilters: {
export const esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
};
// Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -867,7 +867,7 @@ export const esKuery: {
export const esQuery: {
buildEsQuery: typeof buildEsQuery;
getEsQueryConfig: typeof getEsQueryConfig;
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
@@ -1286,22 +1286,19 @@ export interface IFieldType {
visualizable?: boolean;
}
+// Warning: (ae-forgotten-export) The symbol "IndexPatternBase" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public @deprecated (undocumented)
-export interface IIndexPattern {
+export interface IIndexPattern extends IndexPatternBase {
// Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts
//
// (undocumented)
fieldFormatMap?: Record | undefined>;
- // (undocumented)
- fields: IFieldType[];
getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat;
// (undocumented)
getTimeField?(): IFieldType | undefined;
// (undocumented)
- id?: string;
- // (undocumented)
timeFieldName?: string;
// (undocumented)
title: string;
@@ -1351,6 +1348,7 @@ export interface IKibanaSearchRequest {
export interface IKibanaSearchResponse {
id?: string;
isPartial?: boolean;
+ isRestored?: boolean;
isRunning?: boolean;
loaded?: number;
rawResponse: RawResponse;
@@ -2730,13 +2728,13 @@ export interface WaitUntilNextSessionCompletesOptions {
// Warnings were encountered during analysis:
//
-// src/plugins/data/common/es_query/filters/exists_filter.ts:19:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/exists_filter.ts:21:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/match_all_filter.ts:17:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/phrase_filter.ts:23:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/phrases_filter.ts:21:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/query/filter_manager/index.ts b/src/plugins/data/public/query/filter_manager/index.ts
index 327b9763541ac..55dba640b07b6 100644
--- a/src/plugins/data/public/query/filter_manager/index.ts
+++ b/src/plugins/data/public/query/filter_manager/index.ts
@@ -11,3 +11,5 @@ export { FilterManager } from './filter_manager';
export { mapAndFlattenFilters } from './lib/map_and_flatten_filters';
export { onlyDisabledFiltersChanged } from './lib/only_disabled';
export { generateFilters } from './lib/generate_filters';
+export { getDisplayValueFromFilter } from './lib/get_display_value';
+export { getIndexPatternFromFilter } from './lib/get_index_pattern_from_filter';
diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
similarity index 95%
rename from src/plugins/data/common/es_query/filters/get_display_value.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
index ee719843ae879..45c6167f600bc 100644
--- a/src/plugins/data/common/es_query/filters/get_display_value.ts
+++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
@@ -7,9 +7,8 @@
*/
import { i18n } from '@kbn/i18n';
-import { IIndexPattern } from '../..';
+import { Filter, IIndexPattern } from '../../../../common';
import { getIndexPatternFromFilter } from './get_index_pattern_from_filter';
-import { Filter } from '../filters';
function getValueFormatter(indexPattern?: IIndexPattern, key?: string) {
// checking getFormatterForField exists because there is at least once case where an index pattern
diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts
similarity index 100%
rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts
diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
similarity index 88%
rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
index bceeb5f2793ec..7a2ce29102e51 100644
--- a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts
+++ b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
@@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
-import { Filter } from '../filters';
-import { IIndexPattern } from '../..';
+import { Filter, IIndexPattern } from '../../../../common';
export function getIndexPatternFromFilter(
filter: Filter,
diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts
index 82c9e04b79798..fcdea8dec1c2e 100644
--- a/src/plugins/data/public/search/errors/index.ts
+++ b/src/plugins/data/public/search/errors/index.ts
@@ -12,3 +12,4 @@ export * from './timeout_error';
export * from './utils';
export * from './types';
export * from './http_error';
+export * from './search_session_incomplete_warning';
diff --git a/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx
new file mode 100644
index 0000000000000..c5c5c37f31cf8
--- /dev/null
+++ b/src/plugins/data/public/search/errors/search_session_incomplete_warning.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { CoreStart } from 'kibana/public';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const SearchSessionIncompleteWarning = (docLinks: CoreStart['docLinks']) => (
+ <>
+
+ It needs more time to fully render. You can wait here or come back to it later.
+
+
+
+
+
+
+ >
+);
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
index fe66d4b6e9937..155638250a2a4 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
@@ -29,6 +29,12 @@ jest.mock('./utils', () => ({
}),
}));
+jest.mock('../errors/search_session_incomplete_warning', () => ({
+ SearchSessionIncompleteWarning: jest.fn(),
+}));
+
+import { SearchSessionIncompleteWarning } from '../errors/search_session_incomplete_warning';
+
let searchInterceptor: SearchInterceptor;
let mockCoreSetup: MockedKeys;
let bfetchSetup: jest.Mocked;
@@ -508,6 +514,7 @@ describe('SearchInterceptor', () => {
}
: null
);
+ sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore);
fetchMock.mockResolvedValue({ result: 200 });
};
@@ -562,6 +569,92 @@ describe('SearchInterceptor', () => {
(sessionService as jest.Mocked).getSearchOptions
).toHaveBeenCalledWith(sessionId);
});
+
+ test('should not show warning if a search is available during restore', async () => {
+ setup({
+ isRestore: true,
+ isStored: true,
+ sessionId: '123',
+ });
+
+ const responses = [
+ {
+ time: 10,
+ value: {
+ isPartial: false,
+ isRunning: false,
+ isRestored: true,
+ id: 1,
+ rawResponse: {
+ took: 1,
+ },
+ },
+ },
+ ];
+ mockFetchImplementation(responses);
+
+ const response = searchInterceptor.search(
+ {},
+ {
+ sessionId: '123',
+ }
+ );
+ response.subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(0);
+ });
+
+ test('should show warning once if a search is not available during restore', async () => {
+ setup({
+ isRestore: true,
+ isStored: true,
+ sessionId: '123',
+ });
+
+ const responses = [
+ {
+ time: 10,
+ value: {
+ isPartial: false,
+ isRunning: false,
+ isRestored: false,
+ id: 1,
+ rawResponse: {
+ took: 1,
+ },
+ },
+ },
+ ];
+ mockFetchImplementation(responses);
+
+ searchInterceptor
+ .search(
+ {},
+ {
+ sessionId: '123',
+ }
+ )
+ .subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);
+
+ searchInterceptor
+ .search(
+ {},
+ {
+ sessionId: '123',
+ }
+ )
+ .subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);
+ });
});
describe('Session tracking', () => {
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
index 57b156a9b3c00..e0e1df65101c7 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
@@ -43,6 +43,7 @@ import {
PainlessError,
SearchTimeoutError,
TimeoutErrorMode,
+ SearchSessionIncompleteWarning,
} from '../errors';
import { toMountPoint } from '../../../../kibana_react/public';
import { AbortError, KibanaServerError } from '../../../../kibana_utils/public';
@@ -82,6 +83,7 @@ export class SearchInterceptor {
* @internal
*/
private application!: CoreStart['application'];
+ private docLinks!: CoreStart['docLinks'];
private batchedFetch!: BatchedFunc<
{ request: IKibanaSearchRequest; options: ISearchOptionsSerializable },
IKibanaSearchResponse
@@ -95,6 +97,7 @@ export class SearchInterceptor {
this.deps.startServices.then(([coreStart]) => {
this.application = coreStart.application;
+ this.docLinks = coreStart.docLinks;
});
this.batchedFetch = deps.bfetch.batchedFunction({
@@ -345,6 +348,11 @@ export class SearchInterceptor {
this.handleSearchError(e, searchOptions, searchAbortController.isTimeout())
);
}),
+ tap((response) => {
+ if (this.deps.session.isRestore() && response.isRestored === false) {
+ this.showRestoreWarning(this.deps.session.getSessionId());
+ }
+ }),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
if (untrackSearch && this.deps.session.isCurrentSession(sessionId)) {
@@ -371,6 +379,25 @@ export class SearchInterceptor {
}
);
+ private showRestoreWarningToast = (sessionId?: string) => {
+ this.deps.toasts.addWarning(
+ {
+ title: 'Your search session is still running',
+ text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)),
+ },
+ {
+ toastLifeTimeMs: 60000,
+ }
+ );
+ };
+
+ private showRestoreWarning = memoize(
+ this.showRestoreWarningToast,
+ (_: SearchTimeoutError, sessionId: string) => {
+ return sessionId;
+ }
+ );
+
/**
* Show one error notification per session.
* @internal
diff --git a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
index 23de8327ce1f1..9cc9af04409f1 100644
--- a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
+++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
@@ -20,9 +20,9 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
import { IIndexPattern } from '../..';
-import { getDisplayValueFromFilter, Filter } from '../../../common';
+import { Filter } from '../../../common';
import { FilterLabel } from '../filter_bar';
-import { mapAndFlattenFilters } from '../../query';
+import { mapAndFlattenFilters, getDisplayValueFromFilter } from '../../query';
interface Props {
filters: Filter[];
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
index 2b8978a125bca..734161ea87232 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
@@ -37,10 +37,10 @@ import { Operator } from './lib/filter_operators';
import { PhraseValueInput } from './phrase_value_input';
import { PhrasesValuesInput } from './phrases_values_input';
import { RangeValueInput } from './range_value_input';
+import { getIndexPatternFromFilter } from '../../../query';
import { IIndexPattern, IFieldType } from '../../..';
import {
Filter,
- getIndexPatternFromFilter,
FieldFilter,
buildFilter,
buildCustomFilter,
diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
index 9e5090f945182..09e0571c2a870 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
@@ -14,14 +14,13 @@ import { IUiSettingsClient } from 'src/core/public';
import { FilterEditor } from './filter_editor';
import { FilterView } from './filter_view';
import { IIndexPattern } from '../..';
+import { getDisplayValueFromFilter, getIndexPatternFromFilter } from '../../query';
import {
Filter,
isFilterPinned,
- getDisplayValueFromFilter,
toggleFilterNegated,
toggleFilterPinned,
toggleFilterDisabled,
- getIndexPatternFromFilter,
} from '../../../common';
import { getIndexPatterns } from '../../services';
diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
index a0a7e54d27532..0ab3f8a4e3466 100644
--- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
+++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
@@ -176,27 +176,27 @@ exports[`Inspector Data View component should render empty state 1`] = `
+
+
+
+ No data available
+
+
+
-
-
-
- No data available
-
-
-
diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts
index 0764f4f441e42..dd60951e6d228 100644
--- a/src/plugins/data/server/index.ts
+++ b/src/plugins/data/server/index.ts
@@ -238,6 +238,7 @@ export {
DataRequestHandlerContext,
AsyncSearchResponse,
AsyncSearchStatusResponse,
+ NoSearchIdInSessionError,
} from './search';
// Search namespace
diff --git a/src/plugins/data/server/search/errors/no_search_id_in_session.ts b/src/plugins/data/server/search/errors/no_search_id_in_session.ts
new file mode 100644
index 0000000000000..b291df1cee5ba
--- /dev/null
+++ b/src/plugins/data/server/search/errors/no_search_id_in_session.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { KbnError } from '../../../../kibana_utils/common';
+
+export class NoSearchIdInSessionError extends KbnError {
+ constructor() {
+ super('No search ID in this session matching the given search request');
+ }
+}
diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts
index 812f3171aef99..b9affe96ea2dd 100644
--- a/src/plugins/data/server/search/index.ts
+++ b/src/plugins/data/server/search/index.ts
@@ -13,3 +13,4 @@ export * from './strategies/eql_search';
export { usageProvider, SearchUsage, searchUsageObserver } from './collectors';
export * from './aggs';
export * from './session';
+export * from './errors/no_search_id_in_session';
diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts
index 52ee8e60a5b26..314cb2c3acbf8 100644
--- a/src/plugins/data/server/search/search_service.test.ts
+++ b/src/plugins/data/server/search/search_service.test.ts
@@ -25,6 +25,7 @@ import {
ISearchSessionService,
ISearchStart,
ISearchStrategy,
+ NoSearchIdInSessionError,
} from '.';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { expressionsPluginMock } from '../../../expressions/public/mocks';
@@ -175,6 +176,22 @@ describe('Search service', () => {
expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' });
});
+ it('searches even if id is not found in session during restore', async () => {
+ const searchRequest = { params: {} };
+ const options = { sessionId, isStored: true, isRestore: true };
+
+ mockSessionClient.getId = jest.fn().mockImplementation(() => {
+ throw new NoSearchIdInSessionError();
+ });
+
+ const res = await mockScopedClient.search(searchRequest, options).toPromise();
+
+ const [request, callOptions] = mockStrategy.search.mock.calls[0];
+ expect(callOptions).toBe(options);
+ expect(request).toStrictEqual({ ...searchRequest });
+ expect(res.isRestored).toBe(false);
+ });
+
it('does not fail if `trackId` throws', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: false, isRestore: false };
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index a651d7b3bf105..00dffefa5e3a6 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -19,7 +19,7 @@ import {
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
-import { first, switchMap, tap } from 'rxjs/operators';
+import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import type {
@@ -80,6 +80,7 @@ import { registerBsearchRoute } from './routes/bsearch';
import { getKibanaContext } from './expressions/kibana_context';
import { enhancedEsSearchStrategyProvider } from './strategies/ese_search';
import { eqlSearchStrategyProvider } from './strategies/eql_search';
+import { NoSearchIdInSessionError } from './errors/no_search_id_in_session';
type StrategyMap = Record>;
@@ -287,24 +288,48 @@ export class SearchService implements Plugin {
options.strategy
);
- const getSearchRequest = async () =>
- !options.sessionId || !options.isRestore || request.id
- ? request
- : {
+ const getSearchRequest = async () => {
+ if (!options.sessionId || !options.isRestore || request.id) {
+ return request;
+ } else {
+ try {
+ const id = await deps.searchSessionsClient.getId(request, options);
+ this.logger.debug(`Found search session id for request ${id}`);
+ return {
...request,
- id: await deps.searchSessionsClient.getId(request, options),
+ id,
};
+ } catch (e) {
+ if (e instanceof NoSearchIdInSessionError) {
+ this.logger.debug('Ignoring missing search ID');
+ return request;
+ } else {
+ throw e;
+ }
+ }
+ }
+ };
- return from(getSearchRequest()).pipe(
+ const searchRequest$ = from(getSearchRequest());
+ const search$ = searchRequest$.pipe(
switchMap((searchRequest) => strategy.search(searchRequest, options, deps)),
- tap((response) => {
- if (!options.sessionId || !response.id || options.isRestore) return;
+ withLatestFrom(searchRequest$),
+ tap(([response, requestWithId]) => {
+ if (!options.sessionId || !response.id || (options.isRestore && requestWithId.id)) return;
// intentionally swallow tracking error, as it shouldn't fail the search
deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => {
this.logger.error(trackErr);
});
+ }),
+ map(([response, requestWithId]) => {
+ return {
+ ...response,
+ isRestored: !!requestWithId.id,
+ };
})
);
+
+ return search$;
} catch (e) {
return throwError(e);
}
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index c2b533bc42dc6..5ca19f9e1e509 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -447,11 +447,11 @@ export const esFilters: {
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
buildCustomFilter: typeof buildCustomFilter;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter;
buildFilter: typeof buildFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter;
isFilterDisabled: (filter: import("../common").Filter) => boolean;
};
@@ -461,14 +461,14 @@ export const esFilters: {
export const esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
};
// Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const esQuery: {
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
@@ -1205,6 +1205,14 @@ export enum METRIC_TYPES {
TOP_HITS = "top_hits"
}
+// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts
+// Warning: (ae-missing-release-tag) "NoSearchIdInSessionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export class NoSearchIdInSessionError extends KbnError {
+ constructor();
+}
+
// Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -1537,18 +1545,18 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx
new file mode 100644
index 0000000000000..8c32942740a76
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import React from 'react';
+import { EuiSelectable } from '@elastic/eui';
+import { ShallowWrapper } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { shallowWithIntl } from '@kbn/test/jest';
+import { ChangeIndexPattern } from './change_indexpattern';
+import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
+import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
+import { IndexPatternRef } from './types';
+
+function getProps() {
+ return {
+ indexPatternId: indexPatternMock.id,
+ indexPatternRefs: [
+ indexPatternMock as IndexPatternRef,
+ indexPatternWithTimefieldMock as IndexPatternRef,
+ ],
+ onChangeIndexPattern: jest.fn(),
+ trigger: {
+ label: indexPatternMock.title,
+ title: indexPatternMock.title,
+ 'data-test-subj': 'indexPattern-switch-link',
+ },
+ };
+}
+
+function getIndexPatternPickerList(instance: ShallowWrapper) {
+ return instance.find(EuiSelectable).first();
+}
+
+function getIndexPatternPickerOptions(instance: ShallowWrapper) {
+ return getIndexPatternPickerList(instance).prop('options');
+}
+
+export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) {
+ const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions(
+ instance
+ ).map((option: { label: string }) =>
+ option.label === selectedLabel
+ ? { ...option, checked: 'on' }
+ : { ...option, checked: undefined }
+ );
+ return getIndexPatternPickerList(instance).prop('onChange')!(options);
+}
+
+describe('ChangeIndexPattern', () => {
+ test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => {
+ const props = getProps();
+ const comp = shallowWithIntl( );
+ await act(async () => {
+ selectIndexPatternPickerOption(comp, indexPatternMock.title);
+ });
+ expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0);
+ });
+ test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => {
+ const props = getProps();
+ const comp = shallowWithIntl( );
+ await act(async () => {
+ selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title);
+ });
+ expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1);
+ expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id);
+ });
+});
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
index d5076e4daa990..5f2f35e2419dd 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
@@ -26,17 +26,17 @@ export type ChangeIndexPatternTriggerProps = EuiButtonProps & {
// TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern
export function ChangeIndexPattern({
- indexPatternRefs,
indexPatternId,
+ indexPatternRefs,
onChangeIndexPattern,
- trigger,
selectableProps,
+ trigger,
}: {
- trigger: ChangeIndexPatternTriggerProps;
+ indexPatternId?: string;
indexPatternRefs: IndexPatternRef[];
onChangeIndexPattern: (newId: string) => void;
- indexPatternId?: string;
selectableProps?: EuiSelectableProps<{ value: string }>;
+ trigger: ChangeIndexPatternTriggerProps;
}) {
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
@@ -86,7 +86,9 @@ export function ChangeIndexPattern({
const choice = (choices.find(({ checked }) => checked) as unknown) as {
value: string;
};
- onChangeIndexPattern(choice.value);
+ if (choice.value !== indexPatternId) {
+ onChangeIndexPattern(choice.value);
+ }
setPopoverIsOpen(false);
}}
searchProps={{
diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
index 60841799b1398..50be2473a441e 100644
--- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
+++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
@@ -144,7 +144,9 @@ describe('Discover flyout', function () {
expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4');
});
- it('allows navigating with arrow keys through documents', () => {
+ // EuiFlyout is mocked in Jest environments.
+ // EUI team to reinstate `onKeyDown`: https://github.com/elastic/eui/issues/4883
+ it.skip('allows navigating with arrow keys through documents', () => {
const props = getProps();
const component = mountWithIntl( );
findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' });
diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
index f40dbbbae1f87..68786871825ac 100644
--- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
+++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
@@ -147,27 +147,27 @@ exports[`Source Viewer component renders error state 1`] = `
/>
+
+
+ An Error Occurred
+
+
-
-
- An Error Occurred
-
-
diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts
index fbe853ec6deb5..3840df4353faf 100644
--- a/src/plugins/discover/public/index.ts
+++ b/src/plugins/discover/public/index.ts
@@ -17,4 +17,6 @@ export function plugin(initializerContext: PluginInitializerContext) {
export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
export { loadSharingDataHelpers } from './shared';
+
export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator';
+export { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator';
diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts
new file mode 100644
index 0000000000000..edbb0663d4aa3
--- /dev/null
+++ b/src/plugins/discover/public/locator.test.ts
@@ -0,0 +1,270 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public';
+import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
+import { FilterStateStore } from '../../data/common';
+import { DiscoverAppLocatorDefinition } from './locator';
+import { SerializableState } from 'src/plugins/kibana_utils/common';
+
+const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
+const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d';
+
+interface SetupParams {
+ useHash?: boolean;
+}
+
+const setup = async ({ useHash = false }: SetupParams = {}) => {
+ const locator = new DiscoverAppLocatorDefinition({
+ useHash,
+ });
+
+ return {
+ locator,
+ };
+};
+
+beforeEach(() => {
+ // @ts-expect-error
+ hashedItemStore.storage = mockStorage;
+});
+
+describe('Discover url generator', () => {
+ test('can create a link to Discover with no state and no saved search', async () => {
+ const { locator } = await setup();
+ const { app, path } = await locator.getLocation({});
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(app).toBe('discover');
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can create a link to a saved search in Discover', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({ savedSearchId });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(path.startsWith(`#/view/${savedSearchId}`)).toBe(true);
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific index pattern', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ index: indexPatternId,
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific time range', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-15m',
+ mode: 'relative',
+ to: 'now',
+ },
+ });
+ });
+
+ test('can specify query', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify local and global filters', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ filters: [
+ {
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.APP_STATE,
+ },
+ },
+ {
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.GLOBAL_STATE,
+ },
+ },
+ ],
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'appState',
+ },
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ expect(_g).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'globalState',
+ },
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ });
+
+ test('can set refresh interval', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ });
+
+ test('can set time range', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ timeRange: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ });
+
+ test('can specify a search session id', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ searchSessionId: '__test__',
+ });
+
+ expect(path).toMatchInlineSnapshot(`"#/?_g=()&_a=()&searchSessionId=__test__"`);
+ expect(path).toContain('__test__');
+ });
+
+ test('can specify columns, interval, sort and savedQuery', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ columns: ['_source'],
+ interval: 'auto',
+ sort: [['timestamp, asc']] as string[][] & SerializableState,
+ savedQuery: '__savedQueryId__',
+ });
+
+ expect(path).toMatchInlineSnapshot(
+ `"#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"`
+ );
+ });
+
+ describe('useHash property', () => {
+ describe('when default useHash is set to false', () => {
+ test('when using default, sets index pattern ID in the generated URL', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(true);
+ });
+
+ test('when enabling useHash, does not set index pattern ID in the generated URL', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ useHash: true,
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(false);
+ });
+ });
+
+ describe('when default useHash is set to true', () => {
+ test('when using default, does not set index pattern ID in the generated URL', async () => {
+ const { locator } = await setup({ useHash: true });
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(false);
+ });
+
+ test('when disabling useHash, sets index pattern ID in the generated URL', async () => {
+ const { locator } = await setup({ useHash: true });
+ const { path } = await locator.getLocation({
+ useHash: false,
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(true);
+ });
+ });
+ });
+});
diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts
new file mode 100644
index 0000000000000..fff89903bc465
--- /dev/null
+++ b/src/plugins/discover/public/locator.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SerializableState } from 'src/plugins/kibana_utils/common';
+import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
+import type { LocatorDefinition, LocatorPublic } from '../../share/public';
+import { esFilters } from '../../data/public';
+import { setStateToKbnUrl } from '../../kibana_utils/public';
+
+export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR';
+
+export interface DiscoverAppLocatorParams extends SerializableState {
+ /**
+ * Optionally set saved search ID.
+ */
+ savedSearchId?: string;
+
+ /**
+ * Optionally set index pattern ID.
+ */
+ indexPatternId?: string;
+
+ /**
+ * Optionally set the time range in the time picker.
+ */
+ timeRange?: TimeRange;
+
+ /**
+ * Optionally set the refresh interval.
+ */
+ refreshInterval?: RefreshInterval & SerializableState;
+
+ /**
+ * Optionally apply filters.
+ */
+ filters?: Filter[];
+
+ /**
+ * Optionally set a query.
+ */
+ query?: Query;
+
+ /**
+ * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
+ * whether to hash the data in the url to avoid url length issues.
+ */
+ useHash?: boolean;
+
+ /**
+ * Background search session id
+ */
+ searchSessionId?: string;
+
+ /**
+ * Columns displayed in the table
+ */
+ columns?: string[];
+
+ /**
+ * Used interval of the histogram
+ */
+ interval?: string;
+
+ /**
+ * Array of the used sorting [[field,direction],...]
+ */
+ sort?: string[][] & SerializableState;
+
+ /**
+ * id of the used saved query
+ */
+ savedQuery?: string;
+}
+
+export type DiscoverAppLocator = LocatorPublic;
+
+export interface DiscoverAppLocatorDependencies {
+ useHash: boolean;
+}
+
+export class DiscoverAppLocatorDefinition implements LocatorDefinition {
+ public readonly id = DISCOVER_APP_LOCATOR;
+
+ constructor(protected readonly deps: DiscoverAppLocatorDependencies) {}
+
+ public readonly getLocation = async (params: DiscoverAppLocatorParams) => {
+ const {
+ useHash = this.deps.useHash,
+ filters,
+ indexPatternId,
+ query,
+ refreshInterval,
+ savedSearchId,
+ timeRange,
+ searchSessionId,
+ columns,
+ savedQuery,
+ sort,
+ interval,
+ } = params;
+ const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
+ const appState: {
+ query?: Query;
+ filters?: Filter[];
+ index?: string;
+ columns?: string[];
+ interval?: string;
+ sort?: string[][];
+ savedQuery?: string;
+ } = {};
+ const queryState: QueryState = {};
+
+ if (query) appState.query = query;
+ if (filters && filters.length)
+ appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
+ if (indexPatternId) appState.index = indexPatternId;
+ if (columns) appState.columns = columns;
+ if (savedQuery) appState.savedQuery = savedQuery;
+ if (sort) appState.sort = sort;
+ if (interval) appState.interval = interval;
+
+ if (timeRange) queryState.time = timeRange;
+ if (filters && filters.length)
+ queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
+ if (refreshInterval) queryState.refreshInterval = refreshInterval;
+
+ let path = `#/${savedSearchPath}`;
+ path = setStateToKbnUrl('_g', queryState, { useHash }, path);
+ path = setStateToKbnUrl('_a', appState, { useHash }, path);
+
+ if (searchSessionId) {
+ path = `${path}&searchSessionId=${searchSessionId}`;
+ }
+
+ return {
+ app: 'discover',
+ path,
+ state: {},
+ };
+ };
+}
diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts
index 0f57c5c0fa138..53160df472a3c 100644
--- a/src/plugins/discover/public/mocks.ts
+++ b/src/plugins/discover/public/mocks.ts
@@ -16,6 +16,12 @@ const createSetupContract = (): Setup => {
docViews: {
addDocView: jest.fn(),
},
+ locator: {
+ getLocation: jest.fn(),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ navigate: jest.fn(),
+ },
};
return setupContract;
};
@@ -26,6 +32,12 @@ const createStartContract = (): Start => {
urlGenerator: ({
createUrl: jest.fn(),
} as unknown) as DiscoverStart['urlGenerator'],
+ locator: {
+ getLocation: jest.fn(),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ navigate: jest.fn(),
+ },
};
return startContract;
};
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 7b4e7bb67c00e..ec89f7516e92d 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -59,6 +59,7 @@ import {
DiscoverUrlGenerator,
SEARCH_SESSION_ID_QUERY_PARAM,
} from './url_generator';
+import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator';
import { SearchEmbeddableFactory } from './application/embeddable';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { replaceUrlHashQuery } from '../../kibana_utils/public/';
@@ -83,17 +84,68 @@ export interface DiscoverSetup {
*/
addDocView(docViewRaw: DocViewInput | DocViewInputFn): void;
};
+
+ /**
+ * `share` plugin URL locator for Discover app. Use it to generate links into
+ * Discover application, for example, navigate:
+ *
+ * ```ts
+ * await plugins.discover.locator.navigate({
+ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
+ * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
+ * timeRange: {
+ * to: 'now',
+ * from: 'now-15m',
+ * mode: 'relative',
+ * },
+ * });
+ * ```
+ *
+ * Generate a location:
+ *
+ * ```ts
+ * const location = await plugins.discover.locator.getLocation({
+ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
+ * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
+ * timeRange: {
+ * to: 'now',
+ * from: 'now-15m',
+ * mode: 'relative',
+ * },
+ * });
+ * ```
+ */
+ readonly locator: undefined | DiscoverAppLocator;
}
export interface DiscoverStart {
savedSearchLoader: SavedObjectLoader;
/**
- * `share` plugin URL generator for Discover app. Use it to generate links into
- * Discover application, example:
+ * @deprecated Use URL locator instead. URL generaotr will be removed.
+ */
+ readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
+
+ /**
+ * `share` plugin URL locator for Discover app. Use it to generate links into
+ * Discover application, for example, navigate:
+ *
+ * ```ts
+ * await plugins.discover.locator.navigate({
+ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
+ * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
+ * timeRange: {
+ * to: 'now',
+ * from: 'now-15m',
+ * mode: 'relative',
+ * },
+ * });
+ * ```
+ *
+ * Generate a location:
*
* ```ts
- * const url = await plugins.discover.urlGenerator.createUrl({
+ * const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
@@ -104,7 +156,7 @@ export interface DiscoverStart {
* });
* ```
*/
- readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
+ readonly locator: undefined | DiscoverAppLocator;
}
/**
@@ -156,7 +208,12 @@ export class DiscoverPlugin
private stopUrlTracking: (() => void) | undefined = undefined;
private servicesInitialized: boolean = false;
private innerAngularInitialized: boolean = false;
+
+ /**
+ * @deprecated
+ */
private urlGenerator?: DiscoverStart['urlGenerator'];
+ private locator?: DiscoverAppLocator;
/**
* why are those functions public? they are needed for some mocha tests
@@ -179,6 +236,15 @@ export class DiscoverPlugin
})
);
}
+
+ if (plugins.share) {
+ this.locator = plugins.share.url.locators.create(
+ new DiscoverAppLocatorDefinition({
+ useHash: core.uiSettings.get('state:storeInSessionStorage'),
+ })
+ );
+ }
+
this.docViewsRegistry = new DocViewsRegistry();
setDocViewsRegistry(this.docViewsRegistry);
this.docViewsRegistry.addDocView({
@@ -323,6 +389,7 @@ export class DiscoverPlugin
docViews: {
addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry),
},
+ locator: this.locator,
};
}
@@ -367,6 +434,7 @@ export class DiscoverPlugin
return {
urlGenerator: this.urlGenerator,
+ locator: this.locator,
savedSearchLoader: createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
savedObjects: plugins.savedObjects,
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
index 0a27b4098681b..732aa35b05237 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
@@ -13,7 +13,7 @@ import { Error } from '../types';
interface Props {
title: React.ReactNode;
- error: Error;
+ error?: Error;
actions?: JSX.Element;
isCentered?: boolean;
}
@@ -32,30 +32,30 @@ export const PageError: React.FunctionComponent = ({
isCentered,
...rest
}) => {
- const {
- error: errorString,
- cause, // wrapEsError() on the server adds a "cause" array
- message,
- } = error;
+ const errorString = error?.error;
+ const cause = error?.cause; // wrapEsError() on the server adds a "cause" array
+ const message = error?.message;
const errorContent = (
{title}}
body={
- <>
- {cause ? message || errorString : {message || errorString}
}
- {cause && (
- <>
-
-
- {cause.map((causeMsg, i) => (
- {causeMsg}
- ))}
-
- >
- )}
- >
+ error && (
+ <>
+ {cause ? message || errorString : {message || errorString}
}
+ {cause && (
+ <>
+
+
+ {cause.map((causeMsg, i) => (
+ {causeMsg}
+ ))}
+
+ >
+ )}
+ >
+ )
}
iconType="alert"
actions={actions}
diff --git a/src/plugins/es_ui_shared/public/components/page_loading/index.ts b/src/plugins/es_ui_shared/public/components/page_loading/index.ts
new file mode 100644
index 0000000000000..3e7b93bb4e7c3
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/page_loading/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { PageLoading } from './page_loading';
diff --git a/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx
new file mode 100644
index 0000000000000..2fb99208e58ac
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText, EuiPageContent } from '@elastic/eui';
+
+export const PageLoading: React.FunctionComponent = ({ children }) => {
+ return (
+
+ }
+ body={{children} }
+ data-test-subj="sectionLoading"
+ />
+
+ );
+};
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index 7b9013c043a0e..ef2e2daa25468 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -17,6 +17,7 @@ import * as XJson from './xjson';
export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor';
+export { PageLoading } from './components/page_loading';
export { SectionLoading } from './components/section_loading';
export { Frequency, CronEditor } from './components/cron_editor';
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
index fc25879b128ec..77ef0903bc6fc 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
@@ -216,7 +216,11 @@ const FieldEditorComponent = ({
Boolean(field?.type) && field?.type !== (updatedType && updatedType[0].value);
return (
-
-
-
- The index pattern associated with this object no longer exists.
-
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+ The index pattern associated with this object no longer exists.
+
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
+
+
@@ -128,29 +138,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
-
-
- A field associated with this object no longer exists in the index pattern.
-
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+ A field associated with this object no longer exists in the index pattern.
+
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
+
+
@@ -207,29 +227,39 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
-
-
- The saved search associated with this object no longer exists.
-
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+ The saved search associated with this object no longer exists.
+
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
+
+
@@ -286,21 +316,31 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
+
+
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap
index a68e8891b5ad1..bd97f2e6bffb1 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap
@@ -2,6 +2,7 @@
exports[`Flyout conflicts should allow conflict resolution 1`] = `
@@ -277,6 +278,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = `
exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
@@ -548,6 +550,7 @@ Array [
exports[`Flyout should render import step 1`] = `
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 62e0cd0504e8e..f6c8d5fb69408 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
@@ -960,7 +960,7 @@ export class Flyout extends Component {
}
return (
-
+
diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts
index 8f5356f6a2201..5ee3156534c5e 100644
--- a/src/plugins/share/public/index.ts
+++ b/src/plugins/share/public/index.ts
@@ -7,7 +7,8 @@
*/
export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants';
-export { LocatorDefinition } from '../common/url_service';
+
+export { LocatorDefinition, LocatorPublic, KibanaLocation } from '../common/url_service';
export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition';
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx
index a24673a4c1245..e757b5fe8f61d 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx
+++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx
@@ -7,7 +7,14 @@
*/
import React, { useCallback, useState } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiToolTip,
+ EuiIconTip,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import useDebounce from 'react-use/lib/useDebounce';
@@ -84,19 +91,32 @@ function DefaultEditorControls({
) : (
-
-
-
+
+
+
+
+
+
+
+
+
+
)}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
index 7d42eb3f40ac5..610b4a91cfd14 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
@@ -128,7 +128,7 @@ export function FieldSelect({
selectedOptions = [{ label: value!, id: 'INVALID_FIELD' }];
}
} else {
- if (value && !selectedOptions.length) {
+ if (value && fields[fieldsSelector] && !selectedOptions.length) {
onChange([]);
}
}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx
index 8e975f9904256..50d3e8c38e389 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx
@@ -36,7 +36,7 @@ describe('ColorPicker', () => {
const props = { ...defaultProps, value: '#68BC00' };
component = mount( );
component.find('.tvbColorPicker button').simulate('click');
- const input = findTestSubject(component, 'topColorPickerInput');
+ const input = findTestSubject(component, 'euiColorPickerInput_top');
expect(input.props().value).toBe('#68BC00');
});
@@ -44,7 +44,7 @@ describe('ColorPicker', () => {
const props = { ...defaultProps, value: 'rgba(85,66,177,1)' };
component = mount( );
component.find('.tvbColorPicker button').simulate('click');
- const input = findTestSubject(component, 'topColorPickerInput');
+ const input = findTestSubject(component, 'euiColorPickerInput_top');
expect(input.props().value).toBe('85,66,177,1');
});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts
similarity index 85%
rename from src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js
rename to src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts
index 15c21e19af2a5..a026b5bb2051e 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-export const reorder = (list, startIndex, endIndex) => {
+export const reorder = (list: unknown[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts
similarity index 100%
rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js
rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts
similarity index 77%
rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js
rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts
index 458866f2098a0..2862fe933bfb7 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts
@@ -6,20 +6,30 @@
* Side Public License, v 1.
*/
-import _ from 'lodash';
-import handlebars from 'handlebars/dist/handlebars';
-import { emptyLabel } from '../../../../common/empty_label';
+import handlebars from 'handlebars';
import { i18n } from '@kbn/i18n';
+import { emptyLabel } from '../../../../common/empty_label';
+
+type CompileOptions = Parameters[1];
-export function replaceVars(str, args = {}, vars = {}) {
+export function replaceVars(
+ str: string,
+ args: Record = {},
+ vars: Record = {},
+ compileOptions: Partial = {}
+) {
try {
- // we need add '[]' for emptyLabel because this value contains special characters. (https://handlebarsjs.com/guide/expressions.html#literal-segments)
+ /** we need add '[]' for emptyLabel because this value contains special characters.
+ * @see (https://handlebarsjs.com/guide/expressions.html#literal-segments) **/
const template = handlebars.compile(str.split(emptyLabel).join(`[${emptyLabel}]`), {
strict: true,
knownHelpersOnly: true,
+ ...compileOptions,
+ });
+ const string = template({
+ ...vars,
+ args,
});
-
- const string = template(_.assign({}, vars, { args }));
return string;
} catch (e) {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js
index 70529be78567d..c1d82a182e509 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js
@@ -6,8 +6,8 @@
* Side Public License, v 1.
*/
-import handlebars from 'handlebars/dist/handlebars';
import { isNumber } from 'lodash';
+import handlebars from 'handlebars';
import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils';
import { inputFormats, outputFormats, isDuration } from '../lib/durations';
import { getFieldFormats } from '../../../services';
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
index 8e59e8e1bb628..097b0a7b5e332 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
@@ -51,7 +51,9 @@ class TimeseriesVisualization extends Component {
};
applyDocTo = (template) => (doc) => {
- const vars = replaceVars(template, null, doc);
+ const vars = replaceVars(template, null, doc, {
+ noEscape: true,
+ });
if (vars instanceof Error) {
this.showToastNotification = vars.error.caused_by;
diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap
index 25ec05c83a8c6..56e2cb1b60f3c 100644
--- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap
+++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap
@@ -14,7 +14,7 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = `
data-euiicon-type="visualizeApp"
/>
{
await PageObjects.settings.clickEditFieldFormat();
await a11y.testAppSnapshot();
+ await PageObjects.settings.clickCloseEditFieldFormatFlyout();
});
it('Advanced settings', async () => {
diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js
index 7612dae338d9f..031171a58718b 100644
--- a/test/functional/apps/context/index.js
+++ b/test/functional/apps/context/index.js
@@ -15,16 +15,18 @@ export default function ({ getService, getPageObjects, loadTestFile }) {
describe('context app', function () {
this.tags('ciGroup1');
- before(async function () {
+ before(async () => {
await browser.setWindowSize(1200, 800);
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
- await esArchiver.load('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
await PageObjects.common.navigateToApp('discover');
});
- after(function unloadMakelogs() {
- return esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
});
loadTestFile(require.resolve('./_context_navigation'));
diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts
index e3e8a20b693f8..cf5532aa6d762 100644
--- a/test/functional/apps/discover/_data_grid_doc_navigation.ts
+++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts
@@ -41,8 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await rowActions[0].click();
});
- const hasDocHit = await testSubjects.exists('doc-hit');
- expect(hasDocHit).to.be(true);
+ await retry.waitFor('hit loaded', async () => {
+ const hasDocHit = await testSubjects.exists('doc-hit');
+ return !!hasDocHit;
+ });
});
// no longer relevant as null field won't be returned in the Fields API response
diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts
index dce6bfba9cd99..c68db8cbd797b 100644
--- a/test/functional/apps/discover/_discover.ts
+++ b/test/functional/apps/discover/_discover.ts
@@ -181,8 +181,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/89550
- describe.skip('query #2, which has an empty time range', () => {
+ describe('query #2, which has an empty time range', () => {
const fromTime = 'Jun 11, 1999 @ 09:22:11.000';
const toTime = 'Jun 12, 1999 @ 11:21:04.000';
@@ -193,8 +192,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should show "no results"', async () => {
- const isVisible = await PageObjects.discover.hasNoResults();
- expect(isVisible).to.be(true);
+ await retry.waitFor('no results screen is displayed', async function () {
+ const isVisible = await PageObjects.discover.hasNoResults();
+ return isVisible === true;
+ });
});
it('should suggest a new time range is picked', async () => {
diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts
index 771dac4d40a64..8d156cb305586 100644
--- a/test/functional/apps/discover/_doc_navigation.ts
+++ b/test/functional/apps/discover/_doc_navigation.ts
@@ -51,8 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await rowActions[1].click();
});
- const hasDocHit = await testSubjects.exists('doc-hit');
- expect(hasDocHit).to.be(true);
+ await retry.waitFor('hit loaded', async () => {
+ const hasDocHit = await testSubjects.exists('doc-hit');
+ return !!hasDocHit;
+ });
});
// no longer relevant as null field won't be returned in the Fields API response
diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts
index c7fe0a94b6019..24b10e1df0495 100644
--- a/test/functional/apps/discover/_huge_fields.ts
+++ b/test/functional/apps/discover/_huge_fields.ts
@@ -15,21 +15,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
- // FLAKY: https://github.com/elastic/kibana/issues/96113
- describe.skip('test large number of fields in sidebar', function () {
+ describe('test large number of fields in sidebar', function () {
before(async function () {
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/huge_fields');
await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false);
- await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields');
- await PageObjects.settings.navigateTo();
await kibanaServer.uiSettings.update({
'timepicker:timeDefaults': `{ "from": "2016-10-05T00:00:00", "to": "2016-10-06T00:00:00"}`,
});
- await PageObjects.settings.createIndexPattern('*huge*', 'date', true);
await PageObjects.common.navigateToApp('discover');
});
it('test_huge data should have expected number of fields', async function () {
- await PageObjects.discover.selectIndexPattern('*huge*');
+ await PageObjects.discover.selectIndexPattern('testhuge*');
// initially this field should not be rendered
const fieldExistsBeforeScrolling = await testSubjects.exists('field-myvar1050');
expect(fieldExistsBeforeScrolling).to.be(false);
@@ -41,8 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
after(async () => {
await security.testUser.restoreDefaults();
- await esArchiver.unload('test/functional/fixtures/es_archiver/large_fields');
- await kibanaServer.uiSettings.replace({});
+ await esArchiver.unload('test/functional/fixtures/es_archiver/huge_fields');
+ await kibanaServer.uiSettings.unset('timepicker:timeDefaults');
});
});
}
diff --git a/test/functional/apps/discover/_source_filters.ts b/test/functional/apps/discover/_source_filters.ts
index f3793dc3e0288..6c6979b39702c 100644
--- a/test/functional/apps/discover/_source_filters.ts
+++ b/test/functional/apps/discover/_source_filters.ts
@@ -23,8 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
defaultIndex: 'logstash-*',
});
- log.debug('load kibana index with default index pattern');
- await esArchiver.load('test/functional/fixtures/es_archiver/visualize_source-filters');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
// and load a set of makelogs data
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
@@ -43,6 +42,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.sleep(1000);
});
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
+ });
+
it('should not get the field referer', async function () {
const fieldNames = await PageObjects.discover.getAllFieldNames();
expect(fieldNames).to.not.contain('referer');
diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts
index 0278955c577a1..6ef0bfd5a09e8 100644
--- a/test/functional/apps/management/_import_objects.ts
+++ b/test/functional/apps/management/_import_objects.ts
@@ -419,14 +419,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'index-pattern-test-1'
);
- await testSubjects.click('pagination-button-next');
+ const flyout = await testSubjects.find('importSavedObjectsFlyout');
+
+ await (await flyout.findByTestSubject('pagination-button-next')).click();
await PageObjects.savedObjects.setOverriddenIndexPatternValue(
'missing-index-pattern-7',
'index-pattern-test-2'
);
- await testSubjects.click('pagination-button-previous');
+ await (await flyout.findByTestSubject('pagination-button-previous')).click();
const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute(
'managementChangeIndexSelection-missing-index-pattern-1',
@@ -435,7 +437,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20');
- await testSubjects.click('pagination-button-next');
+ await (await flyout.findByTestSubject('pagination-button-next')).click();
const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute(
'managementChangeIndexSelection-missing-index-pattern-7',
diff --git a/test/functional/fixtures/es_archiver/huge_fields/data.json.gz b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz
new file mode 100644
index 0000000000000..1ce42c64c53a3
Binary files /dev/null and b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz differ
diff --git a/test/functional/fixtures/es_archiver/huge_fields/mappings.json b/test/functional/fixtures/es_archiver/huge_fields/mappings.json
new file mode 100644
index 0000000000000..49a677a42f2ba
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/huge_fields/mappings.json
@@ -0,0 +1,24 @@
+{
+ "type": "index",
+ "value": {
+ "index": "testhuge",
+ "mappings": {
+ "properties": {
+ "date": {
+ "type": "date"
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "mapping": {
+ "total_fields": {
+ "limit": "50000"
+ }
+ },
+ "number_of_replicas": "1",
+ "number_of_shards": "5"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json
deleted file mode 100644
index d48aa3e98d18a..0000000000000
--- a/test/functional/fixtures/es_archiver/visualize/data.json
+++ /dev/null
@@ -1,388 +0,0 @@
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:logstash-*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}",
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "timeFieldName": "@timestamp",
- "title": "logstash-*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:logstash*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "title": "logstash*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:long-window-logstash-*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "timeFieldName": "@timestamp",
- "title": "long-window-logstash-*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:Shared-Item-Visualization-AreaChart",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "Shared-Item Visualization AreaChart",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:Visualization-AreaChart",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "Visualization AreaChart",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:68305470-87bc-11e9-a991-3b492a7c3e09",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "control_0_index_pattern",
- "type": "index-pattern"
- },
- {
- "id": "logstash-*",
- "name": "control_1_index_pattern",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "updated_at": "2019-06-05T18:04:48.310Z",
- "visualization": {
- "description": "",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "chained input control",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:64983230-87bf-11e9-a991-3b492a7c3e09",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "control_0_index_pattern",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "updated_at": "2019-06-05T18:26:10.771Z",
- "visualization": {
- "description": "",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "dynamic options input control",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:5d2de430-87c0-11e9-a991-3b492a7c3e09",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "control_0_index_pattern",
- "type": "index-pattern"
- },
- {
- "id": "logstash-*",
- "name": "control_1_index_pattern",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "updated_at": "2019-06-05T18:33:07.827Z",
- "visualization": {
- "description": "",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "chained input control with dynamic options",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:test_index*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]",
- "title": "test_index*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:AreaChart-no-date-field",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "test_index*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "AreaChart [no date field]",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:log*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "title": "log*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:AreaChart-no-time-filter",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "log*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "AreaChart [no time filter]",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:VegaMap",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- ],
- "type": "visualization",
- "visualization": {
- "description": "VegaMap",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "VegaMap",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}"
- }
- },
- "type": "_doc"
- }
-}
\ No newline at end of file
diff --git a/test/functional/fixtures/es_archiver/visualize/mappings.json b/test/functional/fixtures/es_archiver/visualize/mappings.json
deleted file mode 100644
index d032352d9a688..0000000000000
--- a/test/functional/fixtures/es_archiver/visualize/mappings.json
+++ /dev/null
@@ -1,487 +0,0 @@
-{
- "type": "index",
- "value": {
- "aliases": {
- ".kibana_$KIBANA_PACKAGE_VERSION": {},
- ".kibana": {}
- },
- "index": ".kibana_$KIBANA_PACKAGE_VERSION_001",
- "mappings": {
- "_meta": {
- "migrationMappingPropertyHashes": {
- "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd",
- "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724",
- "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724",
- "config": "c63748b75f39d0c54de12d12c1ccbc20",
- "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724",
- "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
- "dashboard": "40554caf09725935e2c02e02563a2d07",
- "index-pattern": "45915a1ad866812242df474eb0479052",
- "kql-telemetry": "d12a98a6f19a2d273696597547e064ee",
- "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a",
- "migrationVersion": "4a1746014a75ade3a714e1db5763276f",
- "namespace": "2f4316de49999235636386fe51dc06c1",
- "namespaces": "2f4316de49999235636386fe51dc06c1",
- "originId": "2f4316de49999235636386fe51dc06c1",
- "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9",
- "references": "7997cf5a56cc02bdc9c93361bde732b0",
- "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4",
- "search": "db2c00e39b36f40930a3b9fc71c823e1",
- "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724",
- "telemetry": "36a616f7026dfa617d6655df850fe16d",
- "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf",
- "type": "2f4316de49999235636386fe51dc06c1",
- "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3",
- "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3",
- "updated_at": "00da57df13e94e9d98437d13ace4bfe0",
- "url": "c7f66a0df8b1b52f17c28c4adb111105",
- "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4",
- "visualization": "f819cf6636b75c9e76ba733a0c6ef355"
- }
- },
- "dynamic": "strict",
- "properties": {
- "application_usage_daily": {
- "dynamic": "false",
- "properties": {
- "timestamp": {
- "type": "date"
- }
- }
- },
- "application_usage_totals": {
- "dynamic": "false",
- "type": "object"
- },
- "application_usage_transactional": {
- "dynamic": "false",
- "type": "object"
- },
- "config": {
- "dynamic": "false",
- "properties": {
- "buildNum": {
- "type": "keyword"
- }
- }
- },
- "core-usage-stats": {
- "dynamic": "false",
- "type": "object"
- },
- "coreMigrationVersion": {
- "type": "keyword"
- },
- "dashboard": {
- "properties": {
- "description": {
- "type": "text"
- },
- "hits": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "optionsJSON": {
- "index": false,
- "type": "text"
- },
- "panelsJSON": {
- "index": false,
- "type": "text"
- },
- "refreshInterval": {
- "properties": {
- "display": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "pause": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "section": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "value": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- }
- }
- },
- "timeFrom": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "timeRestore": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "timeTo": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "index-pattern": {
- "dynamic": "false",
- "properties": {
- "title": {
- "type": "text"
- },
- "type": {
- "type": "keyword"
- }
- }
- },
- "kql-telemetry": {
- "properties": {
- "optInCount": {
- "type": "long"
- },
- "optOutCount": {
- "type": "long"
- }
- }
- },
- "legacy-url-alias": {
- "dynamic": "false",
- "properties": {
- "disabled": {
- "type": "boolean"
- },
- "sourceId": {
- "type": "keyword"
- },
- "targetType": {
- "type": "keyword"
- }
- }
- },
- "migrationVersion": {
- "dynamic": "true",
- "properties": {
- "index-pattern": {
- "fields": {
- "keyword": {
- "ignore_above": 256,
- "type": "keyword"
- }
- },
- "type": "text"
- },
- "visualization": {
- "fields": {
- "keyword": {
- "ignore_above": 256,
- "type": "keyword"
- }
- },
- "type": "text"
- }
- }
- },
- "namespace": {
- "type": "keyword"
- },
- "namespaces": {
- "type": "keyword"
- },
- "originId": {
- "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": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "description": {
- "type": "text"
- },
- "grid": {
- "enabled": false,
- "type": "object"
- },
- "hideChart": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "hits": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "sort": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "search-telemetry": {
- "dynamic": "false",
- "type": "object"
- },
- "server": {
- "dynamic": "false",
- "type": "object"
- },
- "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"
- }
- }
- },
- "type": {
- "type": "keyword"
- },
- "ui-counter": {
- "properties": {
- "count": {
- "type": "integer"
- }
- }
- },
- "ui-metric": {
- "properties": {
- "count": {
- "type": "integer"
- }
- }
- },
- "updated_at": {
- "type": "date"
- },
- "url": {
- "properties": {
- "accessCount": {
- "type": "long"
- },
- "accessDate": {
- "type": "date"
- },
- "createDate": {
- "type": "date"
- },
- "url": {
- "fields": {
- "keyword": {
- "ignore_above": 2048,
- "type": "keyword"
- }
- },
- "type": "text"
- }
- }
- },
- "usage-counters": {
- "dynamic": "false",
- "properties": {
- "domainId": {
- "type": "keyword"
- }
- }
- },
- "visualization": {
- "properties": {
- "description": {
- "type": "text"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "savedSearchRefName": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "uiStateJSON": {
- "index": false,
- "type": "text"
- },
- "version": {
- "type": "integer"
- },
- "visState": {
- "index": false,
- "type": "text"
- }
- }
- }
- }
- },
- "settings": {
- "index": {
- "auto_expand_replicas": "0-1",
- "number_of_replicas": "0",
- "number_of_shards": "1",
- "priority": "10",
- "refresh_interval": "1s",
- "routing_partition_size": "1"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json
index 758841e8d81ef..660da856964b4 100644
--- a/test/functional/fixtures/kbn_archiver/visualize.json
+++ b/test/functional/fixtures/kbn_archiver/visualize.json
@@ -6,14 +6,14 @@
"timeFieldName": "@timestamp",
"title": "logstash-*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "logstash-*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzI2LDJd"
+ "version": "WzEzLDFd"
}
{
@@ -27,10 +27,10 @@
"version": 1,
"visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "5d2de430-87c0-11e9-a991-3b492a7c3e09",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -46,7 +46,7 @@
],
"type": "visualization",
"updated_at": "2019-06-05T18:33:07.827Z",
- "version": "WzMzLDJd"
+ "version": "WzIwLDFd"
}
{
@@ -60,10 +60,10 @@
"version": 1,
"visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "64983230-87bf-11e9-a991-3b492a7c3e09",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -74,7 +74,7 @@
],
"type": "visualization",
"updated_at": "2019-06-05T18:26:10.771Z",
- "version": "WzMyLDJd"
+ "version": "WzE5LDFd"
}
{
@@ -88,10 +88,10 @@
"version": 1,
"visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "68305470-87bc-11e9-a991-3b492a7c3e09",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -107,7 +107,7 @@
],
"type": "visualization",
"updated_at": "2019-06-05T18:04:48.310Z",
- "version": "WzMxLDJd"
+ "version": "WzE4LDFd"
}
{
@@ -115,10 +115,14 @@
"fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]",
"title": "test_index*"
},
+ "coreMigrationVersion": "7.14.0",
"id": "test_index*",
+ "migrationVersion": {
+ "index-pattern": "7.11.0"
+ },
"references": [],
"type": "index-pattern",
- "version": "WzI1LDJd"
+ "version": "WzIxLDFd"
}
{
@@ -132,10 +136,10 @@
"version": 1,
"visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "AreaChart-no-date-field",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -145,7 +149,7 @@
}
],
"type": "visualization",
- "version": "WzM0LDJd"
+ "version": "WzIyLDFd"
}
{
@@ -154,14 +158,14 @@
"fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
"title": "log*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "log*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzM1LDJd"
+ "version": "WzIzLDFd"
}
{
@@ -175,10 +179,10 @@
"version": 1,
"visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "AreaChart-no-time-filter",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -188,7 +192,7 @@
}
],
"type": "visualization",
- "version": "WzM2LDJd"
+ "version": "WzI0LDFd"
}
{
@@ -202,10 +206,10 @@
"version": 1,
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "Shared-Item-Visualization-AreaChart",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -215,7 +219,7 @@
}
],
"type": "visualization",
- "version": "WzI5LDJd"
+ "version": "WzE2LDFd"
}
{
@@ -229,14 +233,14 @@
"version": 1,
"visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "VegaMap",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [],
"type": "visualization",
- "version": "WzM3LDJd"
+ "version": "WzI1LDFd"
}
{
@@ -250,10 +254,10 @@
"version": 1,
"visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "Visualization-AreaChart",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -263,7 +267,7 @@
}
],
"type": "visualization",
- "version": "WzMwLDJd"
+ "version": "WzE3LDFd"
}
{
@@ -272,14 +276,14 @@
"fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
"title": "logstash*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "logstash*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzI3LDJd"
+ "version": "WzE0LDFd"
}
{
@@ -289,12 +293,12 @@
"timeFieldName": "@timestamp",
"title": "long-window-logstash-*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "long-window-logstash-*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzI4LDJd"
+ "version": "WzE1LDFd"
}
\ No newline at end of file
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index 88951bb04c956..cb8f198177017 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -739,6 +739,10 @@ export class SettingsPageObject extends FtrService {
await this.testSubjects.click('editFieldFormat');
}
+ async clickCloseEditFieldFormatFlyout() {
+ await this.testSubjects.click('euiFlyoutCloseButton');
+ }
+
async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) {
await this.find.clickByCssSelector(
`select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] >
diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts
index 6e263dd1cdbbf..7f1ea64bcd979 100644
--- a/test/functional/page_objects/visual_builder_page.ts
+++ b/test/functional/page_objects/visual_builder_page.ts
@@ -563,7 +563,7 @@ export class VisualBuilderPageObject extends FtrService {
public async checkColorPickerPopUpIsPresent(): Promise {
this.log.debug(`Check color picker popup is present`);
- await this.testSubjects.existOrFail('colorPickerPopover', { timeout: 5000 });
+ await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 });
}
public async changePanelPreview(nth: number = 0): Promise {
diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts
index 9aca790b0b437..4340f16492a7c 100644
--- a/test/functional/services/dashboard/panel_actions.ts
+++ b/test/functional/services/dashboard/panel_actions.ts
@@ -211,36 +211,29 @@ export class DashboardPanelActionsService extends FtrService {
await this.testSubjects.click('confirmSaveSavedObjectButton');
}
- async expectExistsRemovePanelAction() {
- this.log.debug('expectExistsRemovePanelAction');
- await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
- }
-
- async expectExistsPanelAction(testSubject: string) {
+ async expectExistsPanelAction(testSubject: string, title?: string) {
this.log.debug('expectExistsPanelAction', testSubject);
- await this.openContextMenu();
- if (await this.testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return;
- if (await this.hasContextMenuMoreItem()) {
- await this.clickContextMenuMoreItem();
+
+ const panelWrapper = title ? await this.getPanelHeading(title) : undefined;
+ await this.openContextMenu(panelWrapper);
+
+ if (!(await this.testSubjects.exists(testSubject))) {
+ if (await this.hasContextMenuMoreItem()) {
+ await this.clickContextMenuMoreItem();
+ }
+ await this.testSubjects.existOrFail(testSubject);
}
- await this.testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ);
- await this.toggleContextMenu();
+ await this.toggleContextMenu(panelWrapper);
}
- async expectMissingPanelAction(testSubject: string) {
- this.log.debug('expectMissingPanelAction', testSubject);
- await this.openContextMenu();
- await this.testSubjects.missingOrFail(testSubject);
- if (await this.hasContextMenuMoreItem()) {
- await this.clickContextMenuMoreItem();
- await this.testSubjects.missingOrFail(testSubject);
- }
- await this.toggleContextMenu();
+ async expectExistsRemovePanelAction() {
+ this.log.debug('expectExistsRemovePanelAction');
+ await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
}
- async expectExistsEditPanelAction() {
+ async expectExistsEditPanelAction(title?: string) {
this.log.debug('expectExistsEditPanelAction');
- await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ);
+ await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, title);
}
async expectExistsReplacePanelAction() {
@@ -253,6 +246,22 @@ export class DashboardPanelActionsService extends FtrService {
await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ);
}
+ async expectExistsToggleExpandAction() {
+ this.log.debug('expectExistsToggleExpandAction');
+ await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
+ }
+
+ async expectMissingPanelAction(testSubject: string) {
+ this.log.debug('expectMissingPanelAction', testSubject);
+ await this.openContextMenu();
+ await this.testSubjects.missingOrFail(testSubject);
+ if (await this.hasContextMenuMoreItem()) {
+ await this.clickContextMenuMoreItem();
+ await this.testSubjects.missingOrFail(testSubject);
+ }
+ await this.toggleContextMenu();
+ }
+
async expectMissingEditPanelAction() {
this.log.debug('expectMissingEditPanelAction');
await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ);
@@ -273,11 +282,6 @@ export class DashboardPanelActionsService extends FtrService {
await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
}
- async expectExistsToggleExpandAction() {
- this.log.debug('expectExistsToggleExpandAction');
- await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
- }
-
async getPanelHeading(title: string) {
return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`);
}
diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts
index 9cf7e0deba2fa..f8c37bab02b86 100644
--- a/test/interpreter_functional/test_suites/run_pipeline/index.ts
+++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts
@@ -21,7 +21,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
before(async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
- await esArchiver.load('test/functional/fixtures/es_archiver/visualize_embedding');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
@@ -32,6 +32,12 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
await testSubjects.find('pluginContent');
});
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
+ });
+
loadTestFile(require.resolve('./basic'));
loadTestFile(require.resolve('./tag_cloud'));
loadTestFile(require.resolve('./metric'));
diff --git a/test/plugin_functional/test_suites/core_plugins/status.ts b/test/plugin_functional/test_suites/core_plugins/status.ts
new file mode 100644
index 0000000000000..2b0f15cb39273
--- /dev/null
+++ b/test/plugin_functional/test_suites/core_plugins/status.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import { ServiceStatusLevels } from '../../../../src/core/server';
+import { PluginFunctionalProviderContext } from '../../services';
+
+export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
+ const supertest = getService('supertest');
+ const log = getService('log');
+
+ const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
+ const getStatus = async (pluginName?: string) => {
+ const resp = await supertest.get('/api/status?v8format=true');
+
+ if (pluginName) {
+ return resp.body.status.plugins[pluginName];
+ } else {
+ return resp.body.status.overall;
+ }
+ };
+
+ const setStatus = async (level: T) =>
+ supertest
+ .post(`/internal/core_plugin_a/status/set?level=${level}`)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ describe('status service', () => {
+ // This test must comes first because the timeout only applies to the initial emission
+ it("returns a timeout for status check that doesn't emit after 30s", async () => {
+ let aStatus = await getStatus('corePluginA');
+ expect(aStatus.level).to.eql('unavailable');
+
+ // Status will remain in unavailable due to core services until custom status timesout
+ // Keep polling until that condition ends, up to a timeout
+ const start = Date.now();
+ while ('elasticsearch' in (aStatus.meta?.affectedServices ?? {})) {
+ aStatus = await getStatus('corePluginA');
+ expect(aStatus.level).to.eql('unavailable');
+
+ // If it's been more than 40s, break out of this loop
+ if (Date.now() - start >= 40_000) {
+ throw new Error(`Timed out waiting for status timeout after 40s`);
+ }
+
+ log.info('Waiting for status check to timeout...');
+ await delay(2000);
+ }
+
+ expect(aStatus.summary).to.eql('Status check timed out after 30s');
+ });
+
+ it('propagates status issues to dependencies', async () => {
+ await setStatus('degraded');
+ await delay(1000);
+ expect((await getStatus('corePluginA')).level).to.eql('degraded');
+ expect((await getStatus('corePluginB')).level).to.eql('degraded');
+
+ await setStatus('available');
+ await delay(1000);
+ expect((await getStatus('corePluginA')).level).to.eql('available');
+ expect((await getStatus('corePluginB')).level).to.eql('available');
+ });
+ });
+}
diff --git a/test/plugin_functional/test_suites/custom_visualizations/index.js b/test/plugin_functional/test_suites/custom_visualizations/index.js
index 0998b97da67ff..22b0f21fb983a 100644
--- a/test/plugin_functional/test_suites/custom_visualizations/index.js
+++ b/test/plugin_functional/test_suites/custom_visualizations/index.js
@@ -14,7 +14,7 @@ export default function ({ getService, loadTestFile }) {
describe('custom visualizations', function () {
before(async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
- await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
@@ -22,6 +22,12 @@ export default function ({ getService, loadTestFile }) {
await browser.setWindowSize(1300, 900);
});
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
+ });
+
loadTestFile(require.resolve('./self_changing_vis'));
});
}
diff --git a/test/scripts/test/server_integration.sh b/test/scripts/test/server_integration.sh
index 1ff4a772bb6e0..6ec08c7727e20 100755
--- a/test/scripts/test/server_integration.sh
+++ b/test/scripts/test/server_integration.sh
@@ -12,3 +12,10 @@ checks-reporter-with-killswitch "Server Integration Tests" \
--bail \
--debug \
--kibana-install-dir $KIBANA_INSTALL_DIR
+
+# Tests that must be run against source in order to build test plugins
+checks-reporter-with-killswitch "Status Integration Tests" \
+ node scripts/functional_tests \
+ --config test/server_integration/http/platform/config.status.ts \
+ --bail \
+ --debug \
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json
new file mode 100644
index 0000000000000..36981d446c9f9
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json
@@ -0,0 +1,7 @@
+{
+ "id": "statusPluginA",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "server": true,
+ "ui": false
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json
new file mode 100644
index 0000000000000..5c73bca024f4e
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "status_plugin_a",
+ "version": "1.0.0",
+ "main": "target/test/server_integration/__fixtures__/plugins/status_plugin_a",
+ "kibana": {
+ "version": "kibana",
+ "templateVersion": "1.0.0"
+ },
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "scripts": {
+ "kbn": "node ../../../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc"
+ }
+}
\ No newline at end of file
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts
new file mode 100644
index 0000000000000..cf221c00e32b0
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { StatusPluginAPlugin } from './plugin';
+
+export const plugin = () => new StatusPluginAPlugin();
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts
new file mode 100644
index 0000000000000..b2e4f0dd322c4
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { Subject } from 'rxjs';
+import {
+ Plugin,
+ CoreSetup,
+ ServiceStatus,
+ ServiceStatusLevels,
+} from '../../../../../../src/core/server';
+
+export class StatusPluginAPlugin implements Plugin {
+ private status$ = new Subject();
+
+ public setup(core: CoreSetup, deps: {}) {
+ // Set a custom status that will not emit immediately to force a timeout
+ core.status.set(this.status$);
+
+ const router = core.http.createRouter();
+
+ router.post(
+ {
+ path: '/internal/status_plugin_a/status/set',
+ validate: {
+ query: schema.object({
+ level: schema.oneOf([
+ schema.literal('available'),
+ schema.literal('degraded'),
+ schema.literal('unavailable'),
+ schema.literal('critical'),
+ ]),
+ }),
+ },
+ },
+ (context, req, res) => {
+ const { level } = req.query;
+
+ this.status$.next({
+ level: ServiceStatusLevels[level],
+ summary: `statusPluginA is ${level}`,
+ });
+
+ return res.ok();
+ }
+ );
+ }
+
+ public start() {}
+ public stop() {}
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json
new file mode 100644
index 0000000000000..5069db62589c7
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true,
+ "composite": true
+ },
+ "include": [
+ "index.ts",
+ "server/**/*.ts",
+ "../../../../../../typings/**/*",
+ ],
+ "exclude": [],
+ "references": [
+ { "path": "../../../../../src/core/tsconfig.json" }
+ ]
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json
new file mode 100644
index 0000000000000..fa02f42d500af
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "statusPluginB",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "server": true,
+ "ui": false,
+ "requiredPlugins": ["statusPluginA"]
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json
new file mode 100644
index 0000000000000..3799d5d470754
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "status_plugin_b",
+ "version": "1.0.0",
+ "main": "target/test/server_integration/__fixtures__/plugins/status_plugin_b",
+ "kibana": {
+ "version": "kibana",
+ "templateVersion": "1.0.0"
+ },
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "scripts": {
+ "kbn": "node ../../../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc"
+ }
+}
\ No newline at end of file
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts
new file mode 100644
index 0000000000000..2002d234827b9
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { StatusPluginBPlugin } from './plugin';
+
+export const plugin = () => new StatusPluginBPlugin();
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.ts b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.ts
new file mode 100644
index 0000000000000..191e8135f69a9
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Plugin } from 'kibana/server';
+
+export class StatusPluginBPlugin implements Plugin {
+ public setup() {}
+ public start() {}
+ public stop() {}
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json
new file mode 100644
index 0000000000000..224aa42ef68d2
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true,
+ "composite": true
+ },
+ "include": [
+ "index.ts",
+ "server/**/*.ts",
+ "../../../../../typings/**/*",
+ ],
+ "exclude": [],
+ "references": [
+ { "path": "../../../../../src/core/tsconfig.json" }
+ ]
+}
diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts
new file mode 100644
index 0000000000000..8cc76c901f47c
--- /dev/null
+++ b/test/server_integration/http/platform/config.status.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { FtrConfigProviderContext } from '@kbn/test';
+
+/*
+ * These tests exist in a separate configuration because:
+ * 1) It must run as the first test after Kibana launches to clear the unavailable status. A separate config makes this
+ * easier to manage and prevent from breaking.
+ * 2) The other server_integration tests run against a built distributable, however the FTR does not support building
+ * and installing plugins against built Kibana. This test must be run against source only in order to build the
+ * fixture plugins
+ */
+// eslint-disable-next-line import/no-default-export
+export default async function ({ readConfigFile }: FtrConfigProviderContext) {
+ const httpConfig = await readConfigFile(require.resolve('../../config'));
+
+ // Find all folders in __fixtures__/plugins since we treat all them as plugin folder
+ const allFiles = fs.readdirSync(path.resolve(__dirname, '../../__fixtures__/plugins'));
+ const plugins = allFiles.filter((file) =>
+ fs.statSync(path.resolve(__dirname, '../../__fixtures__/plugins', file)).isDirectory()
+ );
+
+ return {
+ testFiles: [
+ // Status test should be first to resolve manually created "unavailable" plugin
+ require.resolve('./status'),
+ ],
+ services: httpConfig.get('services'),
+ servers: httpConfig.get('servers'),
+ junit: {
+ reportName: 'Kibana Platform Status Integration Tests',
+ },
+ esTestCluster: httpConfig.get('esTestCluster'),
+ kbnTestServer: {
+ ...httpConfig.get('kbnTestServer'),
+ serverArgs: [
+ ...httpConfig.get('kbnTestServer.serverArgs'),
+ ...plugins.map(
+ (pluginDir) =>
+ `--plugin-path=${path.resolve(__dirname, '../../__fixtures__/plugins', pluginDir)}`
+ ),
+ ],
+ runOptions: {
+ ...httpConfig.get('kbnTestServer.runOptions'),
+ // Don't wait for Kibana to be completely ready so that we can test the status timeouts
+ wait: /\[Kibana\]\[http\] http server running/,
+ },
+ },
+ };
+}
diff --git a/test/server_integration/http/platform/status.ts b/test/server_integration/http/platform/status.ts
new file mode 100644
index 0000000000000..0dcf82c9bea9e
--- /dev/null
+++ b/test/server_integration/http/platform/status.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import type { ServiceStatus, ServiceStatusLevels } from '../../../../src/core/server';
+import { FtrProviderContext } from '../../services/types';
+
+type ServiceStatusSerialized = Omit & { level: string };
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const retry = getService('retry');
+
+ const getStatus = async (pluginName: string): Promise => {
+ const resp = await supertest.get('/api/status?v8format=true');
+
+ return resp.body.status.plugins[pluginName];
+ };
+
+ const setStatus = async (level: T) =>
+ supertest
+ .post(`/internal/status_plugin_a/status/set?level=${level}`)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ describe('status service', () => {
+ // This test must comes first because the timeout only applies to the initial emission
+ it("returns a timeout for status check that doesn't emit after 30s", async () => {
+ let aStatus = await getStatus('statusPluginA');
+ expect(aStatus.level).to.eql('unavailable');
+
+ // Status will remain in unavailable until the custom status check times out
+ // Keep polling until that condition ends, up to a timeout
+ await retry.waitForWithTimeout(`Status check to timeout`, 40_000, async () => {
+ aStatus = await getStatus('statusPluginA');
+ return aStatus.summary === 'Status check timed out after 30s';
+ });
+
+ expect(aStatus.level).to.eql('unavailable');
+ expect(aStatus.summary).to.eql('Status check timed out after 30s');
+ });
+
+ it('propagates status issues to dependencies', async () => {
+ await setStatus('degraded');
+ await retry.waitForWithTimeout(
+ `statusPluginA status to update`,
+ 5_000,
+ async () => (await getStatus('statusPluginA')).level === 'degraded'
+ );
+ expect((await getStatus('statusPluginA')).level).to.eql('degraded');
+ expect((await getStatus('statusPluginB')).level).to.eql('degraded');
+
+ await setStatus('available');
+ await retry.waitForWithTimeout(
+ `statusPluginA status to update`,
+ 5_000,
+ async () => (await getStatus('statusPluginA')).level === 'available'
+ );
+ expect((await getStatus('statusPluginA')).level).to.eql('available');
+ expect((await getStatus('statusPluginB')).level).to.eql('available');
+ });
+ });
+}
diff --git a/test/tsconfig.json b/test/tsconfig.json
index 3e02283946080..8cf33d93a4067 100644
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -17,7 +17,12 @@
"api_integration/apis/telemetry/fixtures/*.json",
"api_integration/apis/telemetry/fixtures/*.json",
],
- "exclude": ["target/**/*", "plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"],
+ "exclude": [
+ "target/**/*",
+ "interpreter_functional/plugins/**/*",
+ "plugin_functional/plugins/**/*",
+ "server_integration/__fixtures__/plugins/**/*",
+ ],
"references": [
{ "path": "../src/core/tsconfig.json" },
{ "path": "../src/plugins/telemetry_management_section/tsconfig.json" },
@@ -52,5 +57,7 @@
{ "path": "../src/plugins/visualize/tsconfig.json" },
{ "path": "plugin_functional/plugins/core_app_status/tsconfig.json" },
{ "path": "plugin_functional/plugins/core_provider_plugin/tsconfig.json" },
+ { "path": "server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json" },
+ { "path": "server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json" },
]
}
diff --git a/test/visual_regression/tests/vega/vega_map_visualization.ts b/test/visual_regression/tests/vega/vega_map_visualization.ts
index 96b08467e4a8f..d891e7f2bab6b 100644
--- a/test/visual_regression/tests/vega/vega_map_visualization.ts
+++ b/test/visual_regression/tests/vega/vega_map_visualization.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'visualize', 'visChart', 'visEditor', 'vegaChart']);
const visualTesting = getService('visualTesting');
@@ -18,12 +19,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.loadIfNeeded(
'test/functional/fixtures/es_archiver/kibana_sample_data_flights'
);
- await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
});
after(async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/kibana_sample_data_flights');
- await esArchiver.unload('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
});
it('should show map with vega layer', async function () {
diff --git a/tsconfig.json b/tsconfig.json
index c91f7b768a5c4..f6df8fcbb6406 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -70,7 +70,6 @@
{ "path": "./src/plugins/visualize/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_management/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" },
-
{ "path": "./x-pack/plugins/actions/tsconfig.json" },
{ "path": "./x-pack/plugins/alerting/tsconfig.json" },
{ "path": "./x-pack/plugins/apm/tsconfig.json" },
diff --git a/tsconfig.refs.json b/tsconfig.refs.json
index 3baf5c323ef81..e08b50cc055c1 100644
--- a/tsconfig.refs.json
+++ b/tsconfig.refs.json
@@ -105,6 +105,7 @@
{ "path": "./x-pack/plugins/stack_alerts/tsconfig.json" },
{ "path": "./x-pack/plugins/task_manager/tsconfig.json" },
{ "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" },
+ { "path": "./x-pack/plugins/timelines/tsconfig.json" },
{ "path": "./x-pack/plugins/transform/tsconfig.json" },
{ "path": "./x-pack/plugins/translations/tsconfig.json" },
{ "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" },
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 5b4a197eea462..b19e89a599840 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -19,7 +19,7 @@ Table of Contents
- [Usage](#usage)
- [Kibana Actions Configuration](#kibana-actions-configuration)
- [Configuration Options](#configuration-options)
- - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts)
+ - [**allowedHosts** configuration](#allowedhosts-configuration)
- [Configuration Utilities](#configuration-utilities)
- [Action types](#action-types)
- [Methods](#methods)
@@ -54,6 +54,9 @@ Table of Contents
- [`subActionParams (getFields)`](#subactionparams-getfields-2)
- [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes)
- [`subActionParams (severity)`](#subactionparams-severity)
+ - [Swimlane](#swimlane)
+ - [`params`](#params-3)
+ - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-)
- [Command Line Utility](#command-line-utility)
- [Developing New Action Types](#developing-new-action-types)
- [licensing](#licensing)
@@ -102,8 +105,8 @@ This module provides utilities for interacting with the configuration.
| ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed |
| ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . |
| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled |
-| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean |
-| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings |
+| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean |
+| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings |
## Action types
@@ -113,17 +116,17 @@ This module provides utilities for interacting with the configuration.
The following table describes the properties of the `options` object.
-| Property | Description | Type |
-| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
-| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string |
-| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string |
-| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number |
-| minimumLicenseRequired | The license required to use the action type. | string |
+| Property | Description | Type |
+| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
+| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string |
+| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string |
+| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number |
+| minimumLicenseRequired | The license required to use the action type. | string |
| validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation. Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function |
-| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function |
-| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function |
-| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
-| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function |
+| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function |
+| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function |
+| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
+| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function |
**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur.
@@ -133,15 +136,15 @@ This is the primary function for an action type. Whenever the action needs to ex
**executor(options)**
-| Property | Description |
-| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| actionId | The action saved object id that the action type is executing for. |
-| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. |
-| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. |
-| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. |
-| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead.|
-| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in. The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). |
-| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log)
+| Property | Description |
+| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| actionId | The action saved object id that the action type is executing for. |
+| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. |
+| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. |
+| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. |
+| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. |
+| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in. The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). |
+| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) |
### Example
@@ -262,16 +265,16 @@ The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kib
The following table describes the properties of the `incident` object.
-| Property | Description | Type |
-| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- |
-| short_description | The title of the incident. | string |
-| description | The description of the incident. | string _(optional)_ |
+| Property | Description | Type |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- |
+| short_description | The title of the incident. | string |
+| description | The description of the incident. | string _(optional)_ |
| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
-| severity | The severity in ServiceNow. | string _(optional)_ |
-| urgency | The urgency in ServiceNow. | string _(optional)_ |
-| impact | The impact in ServiceNow. | string _(optional)_ |
-| category | The category in ServiceNow. | string _(optional)_ |
-| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
+| severity | The severity in ServiceNow. | string _(optional)_ |
+| urgency | The urgency in ServiceNow. | string _(optional)_ |
+| impact | The impact in ServiceNow. | string _(optional)_ |
+| category | The category in ServiceNow. | string _(optional)_ |
+| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
#### `subActionParams (getFields)`
@@ -311,20 +314,20 @@ The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/ma
The following table describes the properties of the `incident` object.
-| Property | Description | Type |
-| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- |
-| summary | The title of the issue. | string |
-| description | The description of the issue. | string _(optional)_ |
+| Property | Description | Type |
+| ----------- | ------------------------------------------------------------------------------------------------------- | --------------------- |
+| summary | The title of the issue. | string |
+| description | The description of the issue. | string _(optional)_ |
| externalId | The ID of the issue in Jira. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
-| issueType | The ID of the issue type in Jira. | string _(optional)_ |
-| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
-| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ |
-| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ |
+| issueType | The ID of the issue type in Jira. | string _(optional)_ |
+| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
+| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ |
+| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ |
#### `subActionParams (getIncident)`
-| Property | Description | Type |
-| ---------- | --------------------------- | ------ |
+| Property | Description | Type |
+| ---------- | ---------------------------- | ------ |
| externalId | The ID of the issue in Jira. | string |
#### `subActionParams (issueTypes)`
@@ -333,20 +336,20 @@ No parameters for the `issueTypes` subaction. Provide an empty object `{}`.
#### `subActionParams (fieldsByIssueType)`
-| Property | Description | Type |
-| -------- | -------------------------------- | ------ |
+| Property | Description | Type |
+| -------- | --------------------------------- | ------ |
| id | The ID of the issue type in Jira. | string |
#### `subActionParams (issues)`
-| Property | Description | Type |
-| -------- | ----------------------- | ------ |
+| Property | Description | Type |
+| -------- | ------------------------ | ------ |
| title | The title to search for. | string |
#### `subActionParams (issue)`
-| Property | Description | Type |
-| -------- | --------------------------- | ------ |
+| Property | Description | Type |
+| -------- | ---------------------------- | ------ |
| id | The ID of the issue in Jira. | string |
#### `subActionParams (getFields)`
@@ -360,10 +363,10 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/
### `params`
-| Property | Description | Type |
-| --------------- | -------------------------------------------------------------------------------------------------- | ------ |
+| Property | Description | Type |
+| --------------- | ------------------------------------------------------------------------------------------------- | ------ |
| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity. | string |
-| subActionParams | The parameters of the subaction. | object |
+| subActionParams | The parameters of the subaction. | object |
#### `subActionParams (pushToService)`
@@ -374,13 +377,13 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/
The following table describes the properties of the `incident` object.
-| Property | Description | Type |
-| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- |
-| name | The title of the incident. | string _(optional)_ |
-| description | The description of the incident. | string _(optional)_ |
+| Property | Description | Type |
+| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- |
+| name | The title of the incident. | string _(optional)_ |
+| description | The description of the incident. | string _(optional)_ |
| externalId | The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
-| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ |
-| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ |
+| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ |
+| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ |
#### `subActionParams (getFields)`
@@ -394,6 +397,36 @@ No parameters for the `incidentTypes` subaction. Provide an empty object `{}`.
No parameters for the `severity` subaction. Provide an empty object `{}`.
+---
+## Swimlane
+
+
+### `params`
+
+| Property | Description | Type |
+| --------------- | ---------------------------------------------------- | ------ |
+| subAction | The subaction to perform. It can be `pushToService`. | string |
+| subActionParams | The parameters of the subaction. | object |
+
+
+`subActionParams (pushToService)`
+
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The Swimlane incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+
+The following table describes the properties of the `incident` object.
+
+| Property | Description | Type |
+| ----------- | -------------------------------- | ------------------- |
+| alertId | The alert id. | string _(optional)_ |
+| caseId | The case id of the incident. | string _(optional)_ |
+| caseName | The case name of the incident. | string _(optional)_ |
+| description | The description of the incident. | string _(optional)_ |
+| ruleName | The rule name. | string _(optional)_ |
+| severity | The severity of the incident. | string _(optional)_ |
---
# Command Line Utility
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 3b91b07eb30f4..012cd1a58de7e 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -429,7 +429,7 @@ describe('create()', () => {
idleInterval: schema.duration().validate('1h'),
pageSize: 100,
},
- tls: {
+ ssl: {
verificationMode: 'full',
proxyVerificationMode: 'full',
},
@@ -1676,6 +1676,70 @@ describe('execute()', () => {
name: 'my name',
},
});
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ });
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ });
});
});
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 449d218ed5ae0..f8d13cdafa755 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -469,6 +469,7 @@ export class ActionsClient {
actionId,
params,
source,
+ relatedSavedObjects,
}: Omit): Promise> {
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
@@ -476,7 +477,13 @@ export class ActionsClient {
) {
await this.authorization.ensureAuthorized('execute');
}
- return this.actionExecutor.execute({ actionId, params, source, request: this.request });
+ return this.actionExecutor.execute({
+ actionId,
+ params,
+ source,
+ request: this.request,
+ relatedSavedObjects,
+ });
}
public async enqueueExecution(options: EnqueueExecutionOptions): Promise {
diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts
index 19a43951377b6..36298d84acabc 100644
--- a/x-pack/plugins/actions/server/actions_config.mock.ts
+++ b/x-pack/plugins/actions/server/actions_config.mock.ts
@@ -15,7 +15,7 @@ const createActionsConfigMock = () => {
ensureHostnameAllowed: jest.fn().mockReturnValue({}),
ensureUriAllowed: jest.fn().mockReturnValue({}),
ensureActionTypeEnabled: jest.fn().mockReturnValue({}),
- getTLSSettings: jest.fn().mockReturnValue({
+ getSSLSettings: jest.fn().mockReturnValue({
verificationMode: 'full',
}),
getProxySettings: jest.fn().mockReturnValue(undefined),
diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts
index 93dad226e0c99..51cd9e5599472 100644
--- a/x-pack/plugins/actions/server/actions_config.test.ts
+++ b/x-pack/plugins/actions/server/actions_config.test.ts
@@ -37,7 +37,7 @@ const defaultActionsConfig: ActionsConfig = {
idleInterval: schema.duration().validate('1h'),
pageSize: 100,
},
- tls: {
+ ssl: {
proxyVerificationMode: 'full',
verificationMode: 'full',
},
@@ -316,38 +316,38 @@ describe('getProxySettings', () => {
proxyRejectUnauthorizedCertificates: true,
};
let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('full');
const configFalse: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
proxyRejectUnauthorizedCertificates: false,
- tls: {},
+ ssl: {},
};
proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('none');
});
- test('returns proper verificationMode value, based on the TLS proxy configuration', () => {
+ test('returns proper verificationMode value, based on the SSL proxy configuration', () => {
const configTrue: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
- tls: {
+ ssl: {
proxyVerificationMode: 'full',
},
};
let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('full');
const configFalse: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
- tls: {
+ ssl: {
proxyVerificationMode: 'none',
},
};
proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('none');
});
test('returns proxy headers', () => {
@@ -432,13 +432,13 @@ describe('getProxySettings', () => {
customHostSettings: [
{
url: 'https://elastic.co',
- tls: {
+ ssl: {
verificationMode: 'full',
},
},
{
url: 'smtp://elastic.co:123',
- tls: {
+ ssl: {
verificationMode: 'none',
},
smtp: {
@@ -465,24 +465,24 @@ describe('getProxySettings', () => {
});
});
-describe('getTLSSettings', () => {
- test('returns proper verificationMode value, based on the TLS proxy configuration', () => {
+describe('getSSLSettings', () => {
+ test('returns proper verificationMode value, based on the SSL proxy configuration', () => {
const configTrue: ActionsConfig = {
...defaultActionsConfig,
- tls: {
+ ssl: {
verificationMode: 'full',
},
};
- let tlsSettings = getActionsConfigurationUtilities(configTrue).getTLSSettings();
- expect(tlsSettings.verificationMode).toBe('full');
+ let sslSettings = getActionsConfigurationUtilities(configTrue).getSSLSettings();
+ expect(sslSettings.verificationMode).toBe('full');
const configFalse: ActionsConfig = {
...defaultActionsConfig,
- tls: {
+ ssl: {
verificationMode: 'none',
},
};
- tlsSettings = getActionsConfigurationUtilities(configFalse).getTLSSettings();
- expect(tlsSettings.verificationMode).toBe('none');
+ sslSettings = getActionsConfigurationUtilities(configFalse).getSSLSettings();
+ expect(sslSettings.verificationMode).toBe('none');
});
});
diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts
index d25101f8279f8..9ce9439b726d4 100644
--- a/x-pack/plugins/actions/server/actions_config.ts
+++ b/x-pack/plugins/actions/server/actions_config.ts
@@ -14,8 +14,8 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { ActionsConfig, AllowedHosts, EnabledActionTypes, CustomHostSettings } from './config';
import { getCanonicalCustomHostUrl } from './lib/custom_host_settings';
import { ActionTypeDisabledError } from './lib';
-import { ProxySettings, ResponseSettings, TLSSettings } from './types';
-import { getTLSSettingsFromConfig } from './builtin_action_types/lib/get_node_tls_options';
+import { ProxySettings, ResponseSettings, SSLSettings } from './types';
+import { getSSLSettingsFromConfig } from './builtin_action_types/lib/get_node_ssl_options';
export { AllowedHosts, EnabledActionTypes } from './config';
@@ -31,7 +31,7 @@ export interface ActionsConfigurationUtilities {
ensureHostnameAllowed: (hostname: string) => void;
ensureUriAllowed: (uri: string) => void;
ensureActionTypeEnabled: (actionType: string) => void;
- getTLSSettings: () => TLSSettings;
+ getSSLSettings: () => SSLSettings;
getProxySettings: () => undefined | ProxySettings;
getResponseSettings: () => ResponseSettings;
getCustomHostSettings: (targetUrl: string) => CustomHostSettings | undefined;
@@ -94,8 +94,8 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet
proxyBypassHosts: arrayAsSet(config.proxyBypassHosts),
proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts),
proxyHeaders: config.proxyHeaders,
- proxyTLSSettings: getTLSSettingsFromConfig(
- config.tls?.proxyVerificationMode,
+ proxySSLSettings: getSSLSettingsFromConfig(
+ config.ssl?.proxyVerificationMode,
config.proxyRejectUnauthorizedCertificates
),
};
@@ -146,8 +146,8 @@ export function getActionsConfigurationUtilities(
isActionTypeEnabled,
getProxySettings: () => getProxySettingsFromConfig(config),
getResponseSettings: () => getResponseSettingsFromConfig(config),
- getTLSSettings: () =>
- getTLSSettingsFromConfig(config.tls?.verificationMode, config.rejectUnauthorized),
+ getSSLSettings: () =>
+ getSSLSettingsFromConfig(config.ssl?.verificationMode, config.rejectUnauthorized),
ensureUriAllowed(uri: string) {
if (!isUriAllowed(uri)) {
throw new Error(allowListErrorMessage(AllowListingField.URL, uri));
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 98ea436b17f3e..8e9ea1c5e4aa9 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
@@ -285,7 +285,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
@@ -346,7 +346,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
index 10955af2f3b13..5feb47ea6c962 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
@@ -21,6 +21,7 @@ const ACTION_TYPE_IDS = [
'.pagerduty',
'.server-log',
'.slack',
+ '.swimlane',
'.teams',
'.webhook',
];
diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts
index 551d3d02ff05d..07859cba4c371 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts
@@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server';
import { getActionType as getEmailActionType } from './email';
import { getActionType as getIndexActionType } from './es_index';
import { getActionType as getPagerDutyActionType } from './pagerduty';
+import { getActionType as getSwimlaneActionType } from './swimlane';
import { getActionType as getServerLogActionType } from './server_log';
import { getActionType as getSlackActionType } from './slack';
import { getActionType as getWebhookActionType } from './webhook';
@@ -65,6 +66,7 @@ export function registerBuiltInActionTypes({
);
actionTypeRegistry.register(getIndexActionType({ logger }));
actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities }));
+ actionTypeRegistry.register(getSwimlaneActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServerLogActionType({ logger }));
actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
index 3161e97583b72..aa439787ad96f 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
@@ -25,7 +25,7 @@ import {
JiraSecretConfigurationType,
JiraExecutorResultData,
ExecutorSubActionGetFieldsByIssueTypeParams,
- ExecutorSubActionGetIssueTypesParams,
+ ExecutorSubActionCommonFieldsParams,
ExecutorSubActionGetIssuesParams,
ExecutorSubActionGetIssueParams,
ExecutorSubActionGetIncidentParams,
@@ -137,7 +137,7 @@ async function executor(
}
if (subAction === 'issueTypes') {
- const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams;
+ const getIssueTypesParams = subActionParams as ExecutorSubActionCommonFieldsParams;
data = await api.issueTypes({
externalService,
params: getIssueTypesParams,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
index a81dfaeef8175..eb2f540deaa9a 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
@@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
ExternalIncidentServiceSecretConfiguration
);
-export const ExecutorSubActionSchema = schema.oneOf([
- schema.literal('getIncident'),
- schema.literal('pushToService'),
- schema.literal('handshake'),
- schema.literal('issueTypes'),
- schema.literal('fieldsByIssueType'),
-]);
-
export const ExecutorSubActionPushParamsSchema = schema.object({
incident: schema.object({
summary: schema.string(),
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
index f6462bac9d83e..9430d734287d3 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
@@ -155,12 +155,12 @@ describe('Jira service', () => {
).toThrow();
});
- test('throws without username', () => {
+ test('throws without email/username', () => {
expect(() =>
createExternalService(
{
- config: { apiUrl: 'test.com' },
- secrets: { apiToken: '', email: 'elastic@elastic.com' },
+ config: { apiUrl: 'test.com', projectKey: 'CK' },
+ secrets: { apiToken: 'token' },
},
logger,
configurationUtilities
@@ -168,12 +168,12 @@ describe('Jira service', () => {
).toThrow();
});
- test('throws without password', () => {
+ test('throws without apiToken/password', () => {
expect(() =>
createExternalService(
{
- config: { apiUrl: 'test.com' },
- secrets: { apiToken: '', email: undefined },
+ config: { apiUrl: 'test.com', projectKey: 'CK' },
+ secrets: { email: 'elastic@elastic.com' },
},
logger,
configurationUtilities
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
index 89a5551554c4a..74d53901d55d9 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
@@ -16,10 +16,10 @@ import {
ExecutorSubActionGetIncidentParamsSchema,
ExecutorSubActionHandshakeParamsSchema,
ExecutorSubActionGetCapabilitiesParamsSchema,
- ExecutorSubActionGetIssueTypesParamsSchema,
ExecutorSubActionGetFieldsByIssueTypeParamsSchema,
ExecutorSubActionGetIssuesParamsSchema,
ExecutorSubActionGetIssueParamsSchema,
+ ExecutorSubActionCommonFieldsParamsSchema,
} from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { Logger } from '../../../../../../src/core/server';
@@ -124,8 +124,8 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf<
typeof ExecutorSubActionGetCapabilitiesParamsSchema
>;
-export type ExecutorSubActionGetIssueTypesParams = TypeOf<
- typeof ExecutorSubActionGetIssueTypesParamsSchema
+export type ExecutorSubActionCommonFieldsParams = TypeOf<
+ typeof ExecutorSubActionCommonFieldsParamsSchema
>;
export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf<
@@ -157,12 +157,12 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
export interface GetIssueTypesHandlerArgs {
externalService: ExternalService;
- params: ExecutorSubActionGetIssueTypesParams;
+ params: ExecutorSubActionCommonFieldsParams;
}
export interface GetCommonFieldsHandlerArgs {
externalService: ExternalService;
- params: ExecutorSubActionGetIssueTypesParams;
+ params: ExecutorSubActionCommonFieldsParams;
}
export interface GetFieldsByIssueTypeHandlerArgs {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
index ccd5a044971df..292471aaf9b6d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
@@ -75,7 +75,7 @@ describe('request', () => {
test('it have been called with proper proxy agent for a valid url', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://localhost:1212',
@@ -110,7 +110,7 @@ describe('request', () => {
test('it have been called with proper proxy agent for an invalid url', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope:',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -141,7 +141,7 @@ describe('request', () => {
test('it bypasses with proxyBypassHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
@@ -164,7 +164,7 @@ describe('request', () => {
test('it does not bypass with proxyBypassHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
@@ -187,7 +187,7 @@ describe('request', () => {
test('it proxies with proxyOnlyHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
@@ -210,7 +210,7 @@ describe('request', () => {
test('it does not proxy with proxyOnlyHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts
index 235fca005e225..4ed9485e923a7 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts
@@ -86,7 +86,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- tls: {
+ ssl: {
verificationMode: 'none',
},
});
@@ -99,7 +99,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { verificationMode: 'none' } }],
+ customHostSettings: [{ url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
@@ -110,7 +110,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: CA } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
@@ -121,7 +121,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: KIBANA_CRT } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: KIBANA_CRT } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -135,7 +135,7 @@ describe('axios connections', () => {
customHostSettings: [
{
url,
- tls: {
+ ssl: {
certificateAuthoritiesData: CA,
verificationMode: 'none',
},
@@ -151,13 +151,13 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- tls: {
+ ssl: {
verificationMode: 'none',
},
customHostSettings: [
{
url,
- tls: {
+ ssl: {
certificateAuthoritiesData: CA,
},
},
@@ -173,7 +173,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url: otherUrl, tls: { verificationMode: 'none' } }],
+ customHostSettings: [{ url: otherUrl, ssl: { verificationMode: 'none' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -184,7 +184,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: 'garbage' } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: 'garbage' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -196,7 +196,7 @@ describe('axios connections', () => {
const ca = '-----BEGIN CERTIFICATE-----\ngarbage\n-----END CERTIFICATE-----\n';
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: ca } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: ca } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -255,7 +255,7 @@ const BaseActionsConfig: ActionsConfig = {
proxyUrl: undefined,
proxyHeaders: undefined,
proxyRejectUnauthorizedCertificates: true,
- tls: {
+ ssl: {
proxyVerificationMode: 'full',
verificationMode: 'full',
},
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
index 8b4abe86e271a..0c1112da5909f 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
@@ -30,7 +30,7 @@ describe('getCustomAgents', () => {
test('get agents for valid proxy URL', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -44,7 +44,7 @@ describe('getCustomAgents', () => {
test('return default agents for invalid proxy URL', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope: not a valid URL',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -64,7 +64,7 @@ describe('getCustomAgents', () => {
test('returns non-proxy agents for matching proxyBypassHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set([targetHost]),
@@ -78,7 +78,7 @@ describe('getCustomAgents', () => {
test('returns proxy agents for non-matching proxyBypassHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set([targetHost]),
@@ -96,7 +96,7 @@ describe('getCustomAgents', () => {
test('returns proxy agents for matching proxyOnlyHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -110,7 +110,7 @@ describe('getCustomAgents', () => {
test('returns non-proxy agents for non-matching proxyOnlyHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -128,7 +128,7 @@ describe('getCustomAgents', () => {
test('handles custom host settings', () => {
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
certificateAuthoritiesData: 'ca data here',
},
@@ -141,7 +141,7 @@ describe('getCustomAgents', () => {
test('handles custom host settings with proxy', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -149,7 +149,7 @@ describe('getCustomAgents', () => {
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
certificateAuthoritiesData: 'ca data here',
},
@@ -163,12 +163,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "none"', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'none',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'certificate',
},
});
@@ -181,12 +181,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "full"', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'full',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
},
});
@@ -199,12 +199,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "none" with a proxy', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'none',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'full',
},
});
@@ -212,7 +212,7 @@ describe('getCustomAgents', () => {
proxyUrl: 'https://someproxyhost',
// note: this setting doesn't come into play, it's for the connection to
// the proxy, not the target url
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -226,12 +226,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "full" with a proxy', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'full',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
},
});
@@ -239,7 +239,7 @@ describe('getCustomAgents', () => {
proxyUrl: 'https://someproxyhost',
// note: this setting doesn't come into play, it's for the connection to
// the proxy, not the target url
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
index a327ee3ffe931..83d31ae1355d3 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
@@ -11,7 +11,7 @@ import HttpProxyAgent from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { Logger } from '../../../../../../src/core/server';
import { ActionsConfigurationUtilities } from '../../actions_config';
-import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options';
+import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
interface GetCustomAgentsResponse {
httpAgent: HttpAgent | undefined;
@@ -23,14 +23,14 @@ export function getCustomAgents(
logger: Logger,
url: string
): GetCustomAgentsResponse {
- const generalTLSSettings = configurationUtilities.getTLSSettings();
- const agentTLSOptions = getNodeTLSOptions(logger, generalTLSSettings.verificationMode);
+ const generalSSLSettings = configurationUtilities.getSSLSettings();
+ const agentSSLOptions = getNodeSSLOptions(logger, generalSSLSettings.verificationMode);
// the default for rejectUnauthorized is the global setting, which can
// be overridden (below) with a custom host setting
const defaultAgents = {
httpAgent: undefined,
httpsAgent: new HttpsAgent({
- ...agentTLSOptions,
+ ...agentSSLOptions,
}),
};
@@ -43,28 +43,28 @@ export function getCustomAgents(
}
// update the defaultAgents.httpsAgent if configured
- const tlsSettings = customHostSettings?.tls;
+ const sslSettings = customHostSettings?.ssl;
let agentOptions: AgentOptions | undefined;
- if (tlsSettings) {
+ if (sslSettings) {
logger.debug(`Creating customized connection settings for: ${url}`);
agentOptions = defaultAgents.httpsAgent.options;
- if (tlsSettings.certificateAuthoritiesData) {
- agentOptions.ca = tlsSettings.certificateAuthoritiesData;
+ if (sslSettings.certificateAuthoritiesData) {
+ agentOptions.ca = sslSettings.certificateAuthoritiesData;
}
- const tlsSettingsFromConfig = getTLSSettingsFromConfig(
- tlsSettings.verificationMode,
- tlsSettings.rejectUnauthorized
+ const sslSettingsFromConfig = getSSLSettingsFromConfig(
+ sslSettings.verificationMode,
+ sslSettings.rejectUnauthorized
);
// see: src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts
// This is where the global rejectUnauthorized is overridden by a custom host
- const customHostNodeTLSOptions = getNodeTLSOptions(
+ const customHostNodeSSLOptions = getNodeSSLOptions(
logger,
- tlsSettingsFromConfig.verificationMode
+ sslSettingsFromConfig.verificationMode
);
- if (customHostNodeTLSOptions.rejectUnauthorized !== undefined) {
- agentOptions.rejectUnauthorized = customHostNodeTLSOptions.rejectUnauthorized;
+ if (customHostNodeSSLOptions.rejectUnauthorized !== undefined) {
+ agentOptions.rejectUnauthorized = customHostNodeSSLOptions.rejectUnauthorized;
}
}
@@ -107,12 +107,12 @@ export function getCustomAgents(
return defaultAgents;
}
- const proxyNodeTLSOptions = getNodeTLSOptions(
+ const proxyNodeSSLOptions = getNodeSSLOptions(
logger,
- proxySettings.proxyTLSSettings.verificationMode
+ proxySettings.proxySSLSettings.verificationMode
);
// At this point, we are going to use a proxy, so we need new agents.
- // We will though, copy over the calculated tls options from above, into
+ // We will though, copy over the calculated ssl options from above, into
// the https agent.
const httpAgent = new HttpProxyAgent(proxySettings.proxyUrl);
const httpsAgent = (new HttpsProxyAgent({
@@ -121,7 +121,7 @@ export function getCustomAgents(
protocol: proxyUrl.protocol,
headers: proxySettings.proxyHeaders,
// do not fail on invalid certs if value is false
- ...proxyNodeTLSOptions,
+ ...proxyNodeSSLOptions,
}) as unknown) as HttpsAgent;
// vsCode wasn't convinced HttpsProxyAgent is an https.Agent, so we convinced it
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts
similarity index 67%
rename from x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts
rename to x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts
index 7d131985053f1..893191b2ca2b4 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts
@@ -4,35 +4,35 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options';
+import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
import { Logger } from '../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
const logger = loggingSystemMock.create().get() as jest.Mocked;
-describe('getNodeTLSOptions', () => {
- test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "full"', () => {
- const nodeOption = getNodeTLSOptions(logger, 'full');
+describe('getNodeSSLOptions', () => {
+ test('get node.js SSL options: rejectUnauthorized eql true for the verification mode "full"', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'full');
expect(nodeOption).toMatchObject({
rejectUnauthorized: true,
});
});
- test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "certificate"', () => {
- const nodeOption = getNodeTLSOptions(logger, 'certificate');
+ test('get node.js SSL options: rejectUnauthorized eql true for the verification mode "certificate"', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'certificate');
expect(nodeOption.checkServerIdentity).not.toBeNull();
expect(nodeOption.rejectUnauthorized).toBeTruthy();
});
- test('get node.js TLS options: rejectUnauthorized eql false for the verification mode "none"', () => {
- const nodeOption = getNodeTLSOptions(logger, 'none');
+ test('get node.js SSL options: rejectUnauthorized eql false for the verification mode "none"', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'none');
expect(nodeOption).toMatchObject({
rejectUnauthorized: false,
});
});
- test('get node.js TLS options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => {
- const nodeOption = getNodeTLSOptions(logger, 'notexist');
+ test('get node.js SSL options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'notexist');
expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
@@ -46,23 +46,23 @@ describe('getNodeTLSOptions', () => {
});
});
-describe('getTLSSettingsFromConfig', () => {
+describe('getSSLSettingsFromConfig', () => {
test('get verificationMode eql "none" if legacy rejectUnauthorized eql false', () => {
- const nodeOption = getTLSSettingsFromConfig(undefined, false);
+ const nodeOption = getSSLSettingsFromConfig(undefined, false);
expect(nodeOption).toMatchObject({
verificationMode: 'none',
});
});
test('get verificationMode eql "none" if legacy rejectUnauthorized eql true', () => {
- const nodeOption = getTLSSettingsFromConfig(undefined, true);
+ const nodeOption = getSSLSettingsFromConfig(undefined, true);
expect(nodeOption).toMatchObject({
verificationMode: 'full',
});
});
test('get verificationMode eql "certificate", ignore rejectUnauthorized', () => {
- const nodeOption = getTLSSettingsFromConfig('certificate', false);
+ const nodeOption = getSSLSettingsFromConfig('certificate', false);
expect(nodeOption).toMatchObject({
verificationMode: 'certificate',
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts
similarity index 92%
rename from x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts
rename to x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts
index 423e9756b13f8..46e90ec3be697 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts
@@ -6,10 +6,10 @@
*/
import { PeerCertificate } from 'tls';
-import { TLSSettings } from '../../types';
+import { SSLSettings } from '../../types';
import { Logger } from '../../../../../../src/core/server';
-export function getNodeTLSOptions(
+export function getNodeSSLOptions(
logger: Logger,
verificationMode?: string
): {
@@ -44,10 +44,10 @@ export function getNodeTLSOptions(
return agentOptions;
}
-export function getTLSSettingsFromConfig(
+export function getSSLSettingsFromConfig(
verificationMode?: 'none' | 'certificate' | 'full',
rejectUnauthorized?: boolean
-): TLSSettings {
+): SSLSettings {
if (verificationMode) {
return { verificationMode };
} else if (rejectUnauthorized !== undefined) {
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 9bdb2d9481142..3719dd8cd737c 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
@@ -76,7 +76,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://example.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -238,7 +238,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['example.com']),
@@ -272,7 +272,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['not-example.com']),
@@ -308,7 +308,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -344,7 +344,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {},
+ proxySSLSettings: {},
proxyBypassHosts: undefined,
proxyOnlyHosts: new Set(['not-example.com']),
}
@@ -377,7 +377,7 @@ describe('send_email module', () => {
undefined,
{
url: 'smtp://example.com:1025',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'ca cert data goes here',
},
smtp: {
@@ -419,7 +419,7 @@ describe('send_email module', () => {
undefined,
{
url: 'smtp://example.com:1025',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'ca cert data goes here',
rejectUnauthorized: true,
},
@@ -461,13 +461,13 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {},
+ proxySSLSettings: {},
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
},
{
url: 'smtp://example.com:1025',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'ca cert data goes here',
rejectUnauthorized: true,
},
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 9f601840bc982..b32ea7d74f025 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
@@ -12,7 +12,7 @@ import { default as MarkdownIt } from 'markdown-it';
import { Logger } from '../../../../../../src/core/server';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { CustomHostSettings } from '../../config';
-import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options';
+import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
// an email "service" which doesn't actually send, just returns what it would send
export const JSON_TRANSPORT_SERVICE = '__json';
@@ -59,7 +59,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transportConfig: Record = {};
const proxySettings = configurationUtilities.getProxySettings();
- const generalTLSSettings = configurationUtilities.getTLSSettings();
+ const generalSSLSettings = configurationUtilities.getSSLSettings();
if (hasAuth && user != null && password != null) {
transportConfig.auth = {
@@ -92,9 +92,9 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
customHostSettings = configurationUtilities.getCustomHostSettings(`smtp://${host}:${port}`);
if (proxySettings && useProxy) {
- transportConfig.tls = getNodeTLSOptions(
+ transportConfig.tls = getNodeSSLOptions(
logger,
- proxySettings?.proxyTLSSettings.verificationMode
+ proxySettings?.proxySSLSettings.verificationMode
);
transportConfig.proxy = proxySettings.proxyUrl;
transportConfig.headers = proxySettings.proxyHeaders;
@@ -104,25 +104,25 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
// authenticate rarely have valid certs; eg cloud proxy, and npm maildev
transportConfig.tls = { rejectUnauthorized: false };
} else {
- transportConfig.tls = getNodeTLSOptions(logger, generalTLSSettings.verificationMode);
+ transportConfig.tls = getNodeSSLOptions(logger, generalSSLSettings.verificationMode);
}
// finally, allow customHostSettings to override some of the settings
// see: https://nodemailer.com/smtp/
if (customHostSettings) {
const tlsConfig: Record = {};
- const tlsSettings = customHostSettings.tls;
+ const sslSettings = customHostSettings.ssl;
const smtpSettings = customHostSettings.smtp;
- if (tlsSettings?.certificateAuthoritiesData) {
- tlsConfig.ca = tlsSettings?.certificateAuthoritiesData;
+ if (sslSettings?.certificateAuthoritiesData) {
+ tlsConfig.ca = sslSettings?.certificateAuthoritiesData;
}
- const tlsSettingsFromConfig = getTLSSettingsFromConfig(
- tlsSettings?.verificationMode,
- tlsSettings?.rejectUnauthorized
+ const sslSettingsFromConfig = getSSLSettingsFromConfig(
+ sslSettings?.verificationMode,
+ sslSettings?.rejectUnauthorized
);
- const nodeTLSOptions = getNodeTLSOptions(logger, tlsSettingsFromConfig.verificationMode);
+ const nodeTLSOptions = getNodeSSLOptions(logger, sslSettingsFromConfig.verificationMode);
if (!transportConfig.tls) {
transportConfig.tls = { ...tlsConfig, ...nodeTLSOptions };
} else {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
index 9095780fea17c..9f76a236cacd5 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
@@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
ExternalIncidentServiceSecretConfiguration
);
-export const ExecutorSubActionSchema = schema.oneOf([
- schema.literal('getIncident'),
- schema.literal('pushToService'),
- schema.literal('handshake'),
- schema.literal('incidentTypes'),
- schema.literal('severity'),
-]);
-
export const ExecutorSubActionPushParamsSchema = schema.object({
incident: schema.object({
name: schema.string(),
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
index 59b0803d189cd..6fec30803d6d7 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
@@ -24,14 +24,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
ExternalIncidentServiceSecretConfiguration
);
-export const ExecutorSubActionSchema = schema.oneOf([
- schema.literal('getFields'),
- schema.literal('getIncident'),
- schema.literal('pushToService'),
- schema.literal('handshake'),
- schema.literal('getChoices'),
-]);
-
const CommentsSchema = schema.nullable(
schema.arrayOf(
schema.object({
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 4108424e26ac4..7953f0ab365e8 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
@@ -194,7 +194,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -221,7 +221,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['example.com']),
@@ -248,7 +248,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['not-example.com']),
@@ -275,7 +275,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -302,7 +302,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts
new file mode 100644
index 0000000000000..1e633e2175808
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { api } from './api';
+import { ExternalService } from './types';
+import {
+ apiParams,
+ externalServiceMock,
+ recordResponseCreate,
+ recordResponseUpdate,
+} from './mocks';
+import { Logger } from '@kbn/logging';
+
+let mockedLogger: jest.Mocked;
+
+describe('api', () => {
+ let externalService: jest.Mocked;
+
+ beforeEach(() => {
+ externalService = externalServiceMock.create();
+ });
+
+ describe('pushToService', () => {
+ test('it pushes a new record', async () => {
+ const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
+ const res = await api.pushToService({
+ externalService,
+ logger: mockedLogger,
+ params,
+ });
+
+ expect(externalService.createComment).toHaveBeenCalled();
+ expect(externalService.createRecord).toHaveBeenCalled();
+ expect(externalService.updateRecord).not.toHaveBeenCalled();
+
+ expect(res).toEqual({
+ ...recordResponseCreate,
+ comments: [
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ ],
+ });
+ });
+
+ test('it pushes a new record without comment', async () => {
+ const params = {
+ ...apiParams,
+ incident: { ...apiParams.incident, externalId: null },
+ comments: [],
+ };
+ const res = await api.pushToService({
+ externalService,
+ logger: mockedLogger,
+ params,
+ });
+
+ expect(externalService.createComment).not.toHaveBeenCalled();
+ expect(externalService.createRecord).toHaveBeenCalled();
+ expect(res).toEqual(recordResponseCreate);
+ });
+
+ test('updates existing record', async () => {
+ const res = await api.pushToService({
+ externalService,
+ logger: mockedLogger,
+ params: apiParams,
+ });
+
+ expect(externalService.createComment).toHaveBeenCalled();
+ expect(externalService.createRecord).not.toHaveBeenCalled();
+ expect(externalService.updateRecord).toHaveBeenCalled();
+ expect(res).toEqual({
+ ...recordResponseUpdate,
+ comments: [
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ ],
+ });
+ });
+
+ test('it calls createRecord correctly', async () => {
+ const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
+ await api.pushToService({ externalService, params, logger: mockedLogger });
+
+ expect(externalService.createRecord).toHaveBeenCalledWith({
+ incident: {
+ alertId: '123456',
+ caseId: '123456',
+ caseName: 'case name',
+ description: 'case desc',
+ ruleName: 'rule name',
+ severity: 'critical',
+ },
+ });
+ });
+
+ test('it calls createComment correctly', async () => {
+ const mockedToISOString = jest
+ .spyOn(Date.prototype, 'toISOString')
+ .mockReturnValue('2021-06-15T18:02:29.404Z');
+
+ const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
+ await api.pushToService({ externalService, params, logger: mockedLogger });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ createdDate: '2021-06-15T18:02:29.404Z',
+ incidentId: '123456',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ },
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ createdDate: '2021-06-15T18:02:29.404Z',
+ incidentId: '123456',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ },
+ });
+
+ mockedToISOString.mockRestore();
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts
new file mode 100644
index 0000000000000..343a94e52711f
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ExternalServiceIncidentResponse,
+ ExternalServiceApi,
+ Incident,
+ PushToServiceApiHandlerArgs,
+ PushToServiceResponse,
+} from './types';
+
+const pushToServiceHandler = async ({
+ externalService,
+ params,
+}: PushToServiceApiHandlerArgs): Promise => {
+ const { comments } = params;
+ let res: PushToServiceResponse;
+ const { externalId, ...rest } = params.incident;
+ const incident: Incident = rest;
+
+ if (externalId != null) {
+ res = await externalService.updateRecord({
+ incidentId: externalId,
+ incident,
+ });
+ } else {
+ res = await externalService.createRecord({ incident });
+ }
+
+ const createdDate = new Date().toISOString();
+
+ if (comments && Array.isArray(comments) && comments.length > 0) {
+ res.comments = [];
+ for (const currentComment of comments) {
+ const comment = await externalService.createComment({
+ incidentId: res.id,
+ comment: currentComment,
+ createdDate,
+ });
+
+ res.comments = [
+ ...(res.comments ?? []),
+ {
+ commentId: comment.commentId,
+ pushedDate: comment.pushedDate,
+ },
+ ];
+ }
+ }
+
+ return res;
+};
+
+export const api: ExternalServiceApi = {
+ pushToService: pushToServiceHandler,
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts
new file mode 100644
index 0000000000000..c2974ec28486c
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getBodyForEventAction } from './helpers';
+import { mappings } from './mocks';
+
+describe('Create Record Mapping', () => {
+ const appId = '45678';
+
+ test('it maps successfully', () => {
+ const params = {
+ alertId: 'al123',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: 'es3456789',
+ description: 'case desc',
+ externalId: null,
+ };
+
+ const data = getBodyForEventAction(appId, mappings, params);
+ expect(data.applicationId).toEqual(appId);
+ expect(data.id).not.toBeDefined();
+ expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toEqual(params.alertId);
+ expect(data.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName);
+ expect(data.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName);
+ expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId);
+ expect(data.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity);
+ expect(data.values?.[mappings?.descriptionConfig?.id ?? 0]).toEqual(params.description);
+ });
+
+ test('it contains the id if defined', () => {
+ const params = {
+ alertId: 'al123',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: 'es3456789',
+ description: 'case desc',
+ externalId: null,
+ };
+ const data = getBodyForEventAction(appId, mappings, params, '123');
+ expect(data.id).toEqual('123');
+ });
+
+ test('it does not includes null mappings', () => {
+ const params = {
+ alertId: 'al123',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: 'es3456789',
+ description: 'case desc',
+ externalId: null,
+ };
+
+ // @ts-expect-error
+ const data = getBodyForEventAction(appId, { ...mappings, test: null }, params);
+ expect(data.values?.test).not.toBeDefined();
+ });
+
+ test('it converts a numeric values correctly', () => {
+ const params = {
+ alertId: 'thisIsNotANumber',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: '123',
+ description: 'case desc',
+ externalId: null,
+ };
+
+ const data = getBodyForEventAction(
+ appId,
+ {
+ ...mappings,
+ caseIdConfig: { ...mappings.caseIdConfig, fieldType: 'numeric' },
+ alertIdConfig: { ...mappings.alertIdConfig, fieldType: 'numeric' },
+ },
+ params
+ );
+
+ expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toBe(0);
+ expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toBe(123);
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts
new file mode 100644
index 0000000000000..13b2df1c97f16
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CreateRecordParams, Incident, SwimlaneRecordPayload, MappingConfigType } from './types';
+
+type ConfigMapping = Omit;
+
+const mappingKeysToIncidentKeys: Record = {
+ ruleNameConfig: 'ruleName',
+ alertIdConfig: 'alertId',
+ caseIdConfig: 'caseId',
+ caseNameConfig: 'caseName',
+ severityConfig: 'severity',
+ descriptionConfig: 'description',
+};
+
+export const getBodyForEventAction = (
+ applicationId: string,
+ mappingConfig: MappingConfigType,
+ params: CreateRecordParams['incident'],
+ incidentId?: string
+): SwimlaneRecordPayload => {
+ const data: SwimlaneRecordPayload = {
+ applicationId,
+ ...(incidentId ? { id: incidentId } : {}),
+ values: {},
+ };
+
+ return (Object.keys(mappingConfig) as Array).reduce((acc, key) => {
+ const fieldMap = mappingConfig[key];
+
+ if (!fieldMap) {
+ return acc;
+ }
+
+ const { id, fieldType } = fieldMap;
+ const paramName = mappingKeysToIncidentKeys[key];
+ const value = params[paramName];
+
+ if (value) {
+ switch (fieldType) {
+ case 'numeric': {
+ const number = Number(value);
+ return { ...acc, values: { ...acc.values, [id]: isNaN(number) ? 0 : number } };
+ }
+ default: {
+ return { ...acc, values: { ...acc.values, [id]: value } };
+ }
+ }
+ }
+
+ return acc;
+ }, data);
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts
new file mode 100644
index 0000000000000..de5010436b6b3
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { curry } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+import { Logger } from '@kbn/logging';
+import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import {
+ SwimlaneExecutorResultData,
+ SwimlanePublicConfigurationType,
+ SwimlaneSecretConfigurationType,
+ ExecutorParams,
+ ExecutorSubActionPushParams,
+} from './types';
+import { validate } from './validators';
+import {
+ ExecutorParamsSchema,
+ SwimlaneSecretsConfiguration,
+ SwimlaneServiceConfiguration,
+} from './schema';
+import { createExternalService } from './service';
+import { api } from './api';
+
+interface GetActionTypeParams {
+ logger: Logger;
+ configurationUtilities: ActionsConfigurationUtilities;
+}
+
+const supportedSubActions: string[] = ['pushToService'];
+
+// action type definition
+export function getActionType(
+ params: GetActionTypeParams
+): ActionType<
+ SwimlanePublicConfigurationType,
+ SwimlaneSecretConfigurationType,
+ ExecutorParams,
+ SwimlaneExecutorResultData | {}
+> {
+ const { logger, configurationUtilities } = params;
+
+ return {
+ id: '.swimlane',
+ minimumLicenseRequired: 'gold',
+ name: i18n.translate('xpack.actions.builtin.swimlaneTitle', {
+ defaultMessage: 'Swimlane',
+ }),
+ validate: {
+ config: schema.object(SwimlaneServiceConfiguration, {
+ validate: curry(validate.config)(configurationUtilities),
+ }),
+ secrets: schema.object(SwimlaneSecretsConfiguration, {
+ validate: curry(validate.secrets)(configurationUtilities),
+ }),
+ params: ExecutorParamsSchema,
+ },
+ executor: curry(executor)({ logger, configurationUtilities }),
+ };
+}
+
+async function executor(
+ {
+ logger,
+ configurationUtilities,
+ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities },
+ execOptions: ActionTypeExecutorOptions<
+ SwimlanePublicConfigurationType,
+ SwimlaneSecretConfigurationType,
+ ExecutorParams
+ >
+): Promise> {
+ const { actionId, config, params, secrets } = execOptions;
+ const { subAction, subActionParams } = params as ExecutorParams;
+ let data: SwimlaneExecutorResultData | null = null;
+
+ const externalService = createExternalService(
+ {
+ config,
+ secrets,
+ },
+ logger,
+ configurationUtilities
+ );
+
+ if (!api[subAction]) {
+ const errorMessage = `[Action][ExternalService] -> [Swimlane] Unsupported subAction type ${subAction}.`;
+ logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ if (!supportedSubActions.includes(subAction)) {
+ const errorMessage = `[Action][ExternalService] -> [Swimlane] subAction ${subAction} not implemented.`;
+ logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ if (subAction === 'pushToService') {
+ const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
+
+ data = await api.pushToService({
+ externalService,
+ params: pushToServiceParams,
+ logger,
+ });
+
+ logger.debug(`response push to service for incident id: ${data.id}`);
+ }
+
+ return { status: 'ok', data: data ?? {}, actionId };
+}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts
new file mode 100644
index 0000000000000..f9931049d81c2
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ExecutorSubActionPushParams, ExternalService, PushToServiceApiParams } from './types';
+
+export const applicationFields = [
+ {
+ id: 'adnlas',
+ name: 'Severity',
+ key: 'severity',
+ fieldType: 'text',
+ },
+ {
+ id: 'adnfls',
+ name: 'Rule Name',
+ key: 'rule-name',
+ fieldType: 'text',
+ },
+ {
+ id: 'a6sst',
+ name: 'Case Id',
+ key: 'case-id-name',
+ fieldType: 'text',
+ },
+ {
+ id: 'a6fst',
+ name: 'Case Name',
+ key: 'case-name',
+ fieldType: 'text',
+ },
+ {
+ id: 'a6fdf',
+ name: 'Comments',
+ key: 'comments',
+ fieldType: 'notes',
+ },
+ {
+ id: 'a6fde',
+ name: 'Description',
+ key: 'description',
+ fieldType: 'text',
+ },
+ {
+ id: 'dfnkls',
+ name: 'Alert ID',
+ key: 'alert-id',
+ fieldType: 'text',
+ },
+];
+
+export const mappings = {
+ severityConfig: applicationFields[0],
+ ruleNameConfig: applicationFields[1],
+ caseIdConfig: applicationFields[2],
+ caseNameConfig: applicationFields[3],
+ commentsConfig: applicationFields[4],
+ descriptionConfig: applicationFields[5],
+ alertIdConfig: applicationFields[6],
+};
+
+export const getApplicationResponse = { fields: applicationFields };
+
+export const recordResponseCreate = {
+ id: '123456',
+ title: 'neato',
+ url: 'swimlane.com',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+};
+
+export const recordResponseUpdate = {
+ id: '98765',
+ title: 'not neato',
+ url: 'laneswim.com',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+};
+
+export const commentResponse = {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+};
+
+const createMock = (): jest.Mocked => {
+ return {
+ createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)),
+ createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)),
+ updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)),
+ };
+};
+
+const externalServiceMock = {
+ create: createMock,
+};
+
+const executorParams: ExecutorSubActionPushParams = {
+ incident: {
+ ruleName: 'rule name',
+ alertId: '123456',
+ caseName: 'case name',
+ severity: 'critical',
+ caseId: '123456',
+ description: 'case desc',
+ externalId: 'incident-3',
+ },
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ },
+ {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ },
+ ],
+};
+
+const apiParams: PushToServiceApiParams = {
+ ...executorParams,
+};
+
+export { externalServiceMock, executorParams, apiParams };
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts
new file mode 100644
index 0000000000000..7f4bdc8ca6c0d
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { schema } from '@kbn/config-schema';
+
+export const ConfigMap = {
+ id: schema.string(),
+ key: schema.string(),
+ name: schema.string(),
+ fieldType: schema.string(),
+};
+
+export const ConfigMapSchema = schema.object(ConfigMap);
+
+export const ConfigMapping = {
+ ruleNameConfig: schema.nullable(ConfigMapSchema),
+ alertIdConfig: schema.nullable(ConfigMapSchema),
+ caseIdConfig: schema.nullable(ConfigMapSchema),
+ caseNameConfig: schema.nullable(ConfigMapSchema),
+ commentsConfig: schema.nullable(ConfigMapSchema),
+ severityConfig: schema.nullable(ConfigMapSchema),
+ descriptionConfig: schema.nullable(ConfigMapSchema),
+};
+
+export const ConfigMappingSchema = schema.object(ConfigMapping);
+
+export const SwimlaneServiceConfiguration = {
+ apiUrl: schema.string(),
+ appId: schema.string(),
+ connectorType: schema.string(),
+ mappings: ConfigMappingSchema,
+};
+
+export const SwimlaneServiceConfigurationSchema = schema.object(SwimlaneServiceConfiguration);
+
+export const SwimlaneSecretsConfiguration = {
+ apiToken: schema.string(),
+};
+
+export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration);
+
+const SwimlaneFields = {
+ alertId: schema.nullable(schema.string()),
+ ruleName: schema.nullable(schema.string()),
+ caseId: schema.nullable(schema.string()),
+ caseName: schema.nullable(schema.string()),
+ severity: schema.nullable(schema.string()),
+ description: schema.nullable(schema.string()),
+};
+
+export const ExecutorSubActionPushParamsSchema = schema.object({
+ incident: schema.object({
+ ...SwimlaneFields,
+ externalId: schema.nullable(schema.string()),
+ }),
+ comments: schema.nullable(
+ schema.arrayOf(
+ schema.object({
+ comment: schema.string(),
+ commentId: schema.string(),
+ })
+ )
+ ),
+});
+
+export const ExecutorParamsSchema = schema.oneOf([
+ schema.object({
+ subAction: schema.literal('pushToService'),
+ subActionParams: ExecutorSubActionPushParamsSchema,
+ }),
+]);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts
new file mode 100644
index 0000000000000..77f4686f8acd0
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts
@@ -0,0 +1,434 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import axios from 'axios';
+
+import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
+import { Logger } from '../../../../../../src/core/server';
+import { actionsConfigMock } from '../../actions_config.mock';
+import * as utils from '../lib/axios_utils';
+import { createExternalService } from './service';
+import { mappings } from './mocks';
+import { ExternalService } from './types';
+
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+
+jest.mock('axios');
+jest.mock('../lib/axios_utils', () => {
+ const originalUtils = jest.requireActual('../lib/axios_utils');
+ return {
+ ...originalUtils,
+ request: jest.fn(),
+ };
+});
+
+axios.create = jest.fn(() => axios);
+const requestMock = utils.request as jest.Mock;
+const configurationUtilities = actionsConfigMock.create();
+
+describe('Swimlane Service', () => {
+ let service: ExternalService;
+ const config = {
+ apiUrl: 'https://test.swimlane.com/',
+ appId: 'bcq16kdTbz5jlwM6h',
+ connectorType: 'all',
+ mappings,
+ };
+ const apiToken = 'token';
+
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'Private-Token': apiToken,
+ };
+
+ const incident = {
+ ruleName: 'Rule Name',
+ caseId: 'Case Id',
+ caseName: 'Case Name',
+ severity: 'Severity',
+ externalId: null,
+ description: 'Description',
+ alertId: 'Alert Id',
+ };
+
+ const url = config.apiUrl.slice(0, -1);
+
+ beforeAll(() => {
+ service = createExternalService(
+ {
+ // The trailing slash at the end of the url is intended.
+ // All API calls need to have the trailing slash removed.
+ config,
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ );
+ });
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('createExternalService', () => {
+ test('throws without url', () => {
+ expect(() =>
+ createExternalService(
+ {
+ config: {
+ // @ts-ignore
+ apiUrl: null,
+ appId: '99999',
+ mappings,
+ },
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ )
+ ).toThrow();
+ });
+
+ test('throws without app id', () => {
+ expect(() =>
+ createExternalService(
+ {
+ config: {
+ apiUrl: 'test.com',
+ // @ts-ignore
+ appId: null,
+ },
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ )
+ ).toThrow();
+ });
+
+ test('throws without mappings', () => {
+ expect(() =>
+ createExternalService(
+ {
+ config: {
+ apiUrl: 'test.com',
+ appId: '987987',
+ // @ts-ignore
+ mappings: null,
+ },
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ )
+ ).toThrow();
+ });
+
+ test('throws without api token', () => {
+ expect(() => {
+ return createExternalService(
+ {
+ config: { apiUrl: 'test.com', appId: '78978', mappings, connectorType: 'all' },
+ secrets: {
+ // @ts-ignore
+ apiToken: null,
+ },
+ },
+ logger,
+ configurationUtilities
+ );
+ }).toThrow();
+ });
+ });
+
+ describe('createRecord', () => {
+ const data = {
+ id: '123',
+ name: 'title',
+ createdDate: '2021-06-01T17:29:51.092Z',
+ };
+
+ test('it creates a record correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ const res = await service.createRecord({
+ incident,
+ });
+
+ expect(res).toEqual({
+ id: '123',
+ title: 'title',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ url: `${url}/record/${config.appId}/123`,
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ await service.createRecord({
+ incident,
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ headers,
+ data: {
+ applicationId: config.appId,
+ values: {
+ [mappings.ruleNameConfig.id]: 'Rule Name',
+ [mappings.caseNameConfig.id]: 'Case Name',
+ [mappings.caseIdConfig.id]: 'Case Id',
+ [mappings.severityConfig.id]: 'Severity',
+ [mappings.descriptionConfig.id]: 'Description',
+ [mappings.alertIdConfig.id]: 'Alert Id',
+ },
+ },
+ url: `${url}/api/app/${config.appId}/record`,
+ method: 'post',
+ configurationUtilities,
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(service.createRecord({ incident })).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+ });
+
+ describe('updateRecord', () => {
+ const data = {
+ id: '123',
+ name: 'title',
+ modifiedDate: '2021-06-01T17:29:51.092Z',
+ };
+ const incidentId = '123';
+
+ test('it updates a record correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ const res = await service.updateRecord({
+ incident,
+ incidentId,
+ });
+
+ expect(res).toEqual({
+ id: '123',
+ title: 'title',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ url: `${url}/record/${config.appId}/123`,
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ await service.updateRecord({
+ incident,
+ incidentId,
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ headers,
+ data: {
+ applicationId: config.appId,
+ id: incidentId,
+ values: {
+ [mappings.ruleNameConfig.id]: 'Rule Name',
+ [mappings.caseNameConfig.id]: 'Case Name',
+ [mappings.caseIdConfig.id]: 'Case Id',
+ [mappings.severityConfig.id]: 'Severity',
+ [mappings.descriptionConfig.id]: 'Description',
+ [mappings.alertIdConfig.id]: 'Alert Id',
+ },
+ },
+ url: `${url}/api/app/${config.appId}/record/${incidentId}`,
+ method: 'patch',
+ configurationUtilities,
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow(
+ `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+ });
+
+ describe('createComment', () => {
+ const data = {
+ id: '123',
+ name: 'title',
+ modifiedDate: '2021-06-01T17:29:51.092Z',
+ };
+ const incidentId = '123';
+ const comment = { commentId: '456', comment: 'A comment' };
+ const createdDate = '2021-06-01T17:29:51.092Z';
+
+ test('it updates a record correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ const res = await service.createComment({
+ comment,
+ incidentId,
+ createdDate,
+ });
+
+ expect(res).toEqual({
+ commentId: '456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ await service.createComment({
+ comment,
+ incidentId,
+ createdDate,
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ headers,
+ data: {
+ createdDate,
+ fieldId: mappings.commentsConfig.id,
+ isRichText: true,
+ message: comment.comment,
+ },
+ url: `${url}/api/app/${config.appId}/record/${incidentId}/${mappings.commentsConfig.id}/comment`,
+ method: 'post',
+ configurationUtilities,
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(service.createComment({ comment, incidentId, createdDate })).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create comment in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+ });
+
+ describe('error messages', () => {
+ const errorResponse = { ErrorCode: '1', Argument: 'Invalid field' };
+
+ test('it contains the response error', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: errorResponse };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: Invalid field (1)`
+ );
+ });
+
+ test('it shows an empty string for reason if the ErrorCode is undefined', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: { ErrorCode: '1' } };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+
+ test('it shows an empty string for reason if the Argument is undefined', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: { Argument: 'Invalid field' } };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+
+ test('it shows an empty string for reason if data is undefined', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = {};
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+
+ test('it shows the status code', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: errorResponse, status: 400 };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 400. Error: An error has occurred. Reason: Invalid field (1)`
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts
new file mode 100644
index 0000000000000..f68d22121dbcc
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts
@@ -0,0 +1,196 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Logger } from '@kbn/logging';
+import axios from 'axios';
+
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { getErrorMessage, request } from '../lib/axios_utils';
+import { getBodyForEventAction } from './helpers';
+import {
+ CreateCommentParams,
+ CreateRecordParams,
+ ExternalService,
+ ExternalServiceCredentials,
+ ExternalServiceIncidentResponse,
+ MappingConfigType,
+ ResponseError,
+ SwimlanePublicConfigurationType,
+ SwimlaneRecordPayload,
+ SwimlaneSecretConfigurationType,
+ UpdateRecordParams,
+} from './types';
+import * as i18n from './translations';
+
+const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => {
+ if (errorResponse == null) {
+ return 'unknown';
+ }
+
+ const { ErrorCode, Argument } = errorResponse;
+ return Argument != null && ErrorCode != null ? `${Argument} (${ErrorCode})` : 'unknown';
+};
+
+export const createExternalService = (
+ { config, secrets }: ExternalServiceCredentials,
+ logger: Logger,
+ configurationUtilities: ActionsConfigurationUtilities
+): ExternalService => {
+ const { apiUrl: url, appId, mappings } = config as SwimlanePublicConfigurationType;
+ const { apiToken } = secrets as SwimlaneSecretConfigurationType;
+
+ const axiosInstance = axios.create();
+
+ if (!url || !appId || !apiToken || !mappings) {
+ throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
+ }
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ 'Private-Token': `${secrets.apiToken}`,
+ };
+
+ const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
+ const apiUrl = urlWithoutTrailingSlash.endsWith('api')
+ ? urlWithoutTrailingSlash
+ : urlWithoutTrailingSlash + '/api';
+
+ const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`;
+
+ const getPostRecordIdUrl = (id: string, recordId: string) =>
+ `${getPostRecordUrl(id)}/${recordId}`;
+
+ const getRecordIdUrl = (id: string, recordId: string) =>
+ `${urlWithoutTrailingSlash}/record/${id}/${recordId}`;
+
+ const getPostCommentUrl = (id: string, recordId: string, commentFieldId: string) =>
+ `${getPostRecordIdUrl(id, recordId)}/${commentFieldId}/comment`;
+
+ const getCommentFieldId = (fieldMappings: MappingConfigType): string | null =>
+ fieldMappings.commentsConfig?.id || null;
+
+ const createRecord = async (
+ params: CreateRecordParams
+ ): Promise => {
+ try {
+ const mappingConfig = mappings as MappingConfigType;
+ const data = getBodyForEventAction(appId, mappingConfig, params.incident);
+
+ const res = await request({
+ axios: axiosInstance,
+ configurationUtilities,
+ data,
+ headers,
+ logger,
+ method: 'post',
+ url: getPostRecordUrl(appId),
+ });
+ return {
+ id: res.data.id,
+ title: res.data.name,
+ url: getRecordIdUrl(appId, res.data.id),
+ pushedDate: new Date(res.data.createdDate).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to create record in application with id ${appId}. Status: ${
+ error.response?.status ?? 500
+ }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}`
+ )
+ );
+ }
+ };
+
+ const updateRecord = async (
+ params: UpdateRecordParams
+ ): Promise => {
+ try {
+ const mappingConfig = mappings as MappingConfigType;
+ const data = getBodyForEventAction(appId, mappingConfig, params.incident, params.incidentId);
+
+ const res = await request({
+ axios: axiosInstance,
+ configurationUtilities,
+ data,
+ headers,
+ logger,
+ method: 'patch',
+ url: getPostRecordIdUrl(appId, params.incidentId),
+ });
+
+ return {
+ id: res.data.id,
+ title: res.data.name,
+ url: getRecordIdUrl(appId, params.incidentId),
+ pushedDate: new Date(res.data.modifiedDate).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to update record in application with id ${appId}. Status: ${
+ error.response?.status ?? 500
+ }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}`
+ )
+ );
+ }
+ };
+
+ const createComment = async ({ incidentId, comment, createdDate }: CreateCommentParams) => {
+ try {
+ const mappingConfig = mappings as MappingConfigType;
+ const fieldId = getCommentFieldId(mappingConfig);
+
+ if (fieldId == null) {
+ throw new Error(`No comment field mapped in ${i18n.NAME} connector`);
+ }
+
+ const data = {
+ createdDate,
+ fieldId,
+ isRichText: true,
+ message: comment.comment,
+ };
+
+ await request({
+ axios: axiosInstance,
+ configurationUtilities,
+ data,
+ headers,
+ logger,
+ method: 'post',
+ url: getPostCommentUrl(appId, incidentId, fieldId),
+ });
+
+ /**
+ * Swimlane response does not contain any data.
+ * We cannot get an externalCommentId
+ */
+ return {
+ commentId: comment.commentId,
+ pushedDate: createdDate,
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to create comment in application with id ${appId}. Status: ${
+ error.response?.status ?? 500
+ }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}`
+ )
+ );
+ }
+ };
+
+ return {
+ createComment,
+ createRecord,
+ updateRecord,
+ };
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts
new file mode 100644
index 0000000000000..671cf224448f6
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const NAME = i18n.translate('xpack.actions.builtin.case.swimlaneTitle', {
+ defaultMessage: 'Swimlane',
+});
+
+export const ALLOWED_HOSTS_ERROR = (message: string) =>
+ i18n.translate('xpack.actions.builtin.swimlane.configuration.apiAllowedHostsError', {
+ defaultMessage: 'error configuring connector action: {message}',
+ values: {
+ message,
+ },
+ });
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts
new file mode 100644
index 0000000000000..5cb3b10989621
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { TypeOf } from '@kbn/config-schema';
+import { Logger } from '@kbn/logging';
+import {
+ ConfigMappingSchema,
+ ExecutorParamsSchema,
+ ExecutorSubActionPushParamsSchema,
+ SwimlaneSecretsConfigurationSchema,
+ SwimlaneServiceConfigurationSchema,
+} from './schema';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+
+export type SwimlanePublicConfigurationType = TypeOf;
+export type SwimlaneSecretConfigurationType = TypeOf;
+
+export type MappingConfigType = TypeOf;
+export type ExecutorParams = TypeOf;
+export type ExecutorSubActionPushParams = TypeOf;
+
+export interface ExternalServiceCredentials {
+ config: SwimlanePublicConfigurationType;
+ secrets: SwimlaneSecretConfigurationType;
+}
+
+export interface ExternalServiceValidation {
+ config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void;
+ secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void;
+}
+
+export interface CreateRecordParams {
+ incident: Incident;
+}
+export interface UpdateRecordParams extends CreateRecordParams {
+ incidentId: string;
+}
+
+export type PushToServiceApiParams = ExecutorSubActionPushParams;
+export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: PushToServiceApiParams;
+ logger: Logger;
+}
+
+export interface ExternalServiceIncidentResponse {
+ id: string;
+ title: string;
+ url: string;
+ pushedDate: string;
+}
+export interface ExternalServiceCommentResponse {
+ commentId: string;
+ pushedDate: string;
+ externalCommentId?: string;
+}
+
+export interface FieldConfig {
+ id: string;
+ name: string;
+ key: string;
+ fieldType: string;
+}
+
+export interface SwimlaneRecordPayload {
+ applicationId: string;
+ values: SwimlaneDataValues;
+ id?: string;
+}
+
+export interface ExternalService {
+ createComment: (params: CreateCommentParams) => Promise;
+ createRecord: (params: CreateRecordParams) => Promise;
+ updateRecord: (params: UpdateRecordParams) => Promise;
+}
+
+export type Incident = Omit;
+
+export interface ExternalServiceApiHandlerArgs {
+ externalService: ExternalService;
+}
+
+export interface GetApplicationHandlerArgs {
+ externalService: ExternalService;
+}
+
+export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
+ comments?: ExternalServiceCommentResponse[];
+}
+
+export interface ExternalServiceApi {
+ pushToService: (args: PushToServiceApiHandlerArgs) => Promise;
+}
+
+export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse;
+export type SwimlaneDataValues = Record;
+export interface SwimlaneComment {
+ fieldId: string;
+ message: string | number;
+ createdDate: string;
+ isRichText: boolean;
+}
+export type SwimlaneDataComments = Record;
+
+export interface SimpleComment {
+ comment: SwimlaneComment['message'];
+ commentId: string;
+}
+
+export interface CreateCommentParams {
+ incidentId: string;
+ comment: SimpleComment;
+ createdDate: string;
+}
+
+export interface ResponseError {
+ ErrorCode: number;
+ Argument: string;
+}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts
new file mode 100644
index 0000000000000..1972cd7e6af0b
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { ExternalServiceValidation, SwimlanePublicConfigurationType } from './types';
+import * as i18n from './translations';
+
+export const validateCommonConfig = (
+ configurationUtilities: ActionsConfigurationUtilities,
+ configObject: SwimlanePublicConfigurationType
+) => {
+ try {
+ configurationUtilities.ensureUriAllowed(configObject.apiUrl);
+ } catch (allowedListError) {
+ return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message);
+ }
+};
+
+export const validateCommonSecrets = () => {};
+
+export const validate: ExternalServiceValidation = {
+ config: validateCommonConfig,
+ secrets: validateCommonSecrets,
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
index bf34789e03fae..497300b86bdea 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
@@ -170,7 +170,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
@@ -234,7 +234,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
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 b2c865c2f5374..c04c79075abdc 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
@@ -293,7 +293,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
@@ -386,7 +386,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts
index 9774bfb05d4ff..d99b9349e977b 100644
--- a/x-pack/plugins/actions/server/config.test.ts
+++ b/x-pack/plugins/actions/server/config.test.ts
@@ -178,9 +178,9 @@ describe('config validation', () => {
);
});
- test('action with tls configuration', () => {
+ test('action with ssl configuration', () => {
const config: Record = {
- tls: {
+ ssl: {
verificationMode: 'none',
proxyVerificationMode: 'none',
},
@@ -208,7 +208,7 @@ describe('config validation', () => {
"proxyRejectUnauthorizedCertificates": true,
"rejectUnauthorized": true,
"responseTimeout": "PT1M",
- "tls": Object {
+ "ssl": Object {
"proxyVerificationMode": "none",
"verificationMode": "none",
},
diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts
index 8859a2d8881a2..1ae196c25a756 100644
--- a/x-pack/plugins/actions/server/config.ts
+++ b/x-pack/plugins/actions/server/config.ts
@@ -31,7 +31,7 @@ const customHostSettingsSchema = schema.object({
requireTLS: schema.maybe(schema.boolean()),
})
),
- tls: schema.maybe(
+ ssl: schema.maybe(
schema.object({
/**
* @deprecated in favor of `verificationMode`
@@ -78,16 +78,16 @@ export const configSchema = schema.object({
proxyUrl: schema.maybe(schema.string()),
proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())),
/**
- * @deprecated in favor of `tls.proxyVerificationMode`
+ * @deprecated in favor of `ssl.proxyVerificationMode`
**/
proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }),
proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
/**
- * @deprecated in favor of `tls.verificationMode`
+ * @deprecated in favor of `ssl.verificationMode`
**/
rejectUnauthorized: schema.boolean({ defaultValue: true }),
- tls: schema.maybe(
+ ssl: schema.maybe(
schema.object({
verificationMode: schema.maybe(
schema.oneOf(
diff --git a/x-pack/plugins/actions/server/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts
index 508709c8783ab..9163a0d105ce8 100644
--- a/x-pack/plugins/actions/server/constants/event_log.ts
+++ b/x-pack/plugins/actions/server/constants/event_log.ts
@@ -8,5 +8,6 @@
export const EVENT_LOG_PROVIDER = 'actions';
export const EVENT_LOG_ACTIONS = {
execute: 'execute',
+ executeStart: 'execute-start',
executeViaHttp: 'execute-via-http',
};
diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts
index 4cacba6dc880a..ee8064d2aadc5 100644
--- a/x-pack/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.test.ts
@@ -83,6 +83,62 @@ describe('execute()', () => {
});
});
+ test('schedules the action with all given parameters and relatedSavedObjects', async () => {
+ const actionTypeRegistry = actionTypeRegistryMock.create();
+ const executeFn = createExecutionEnqueuerFunction({
+ taskManager: mockTaskManager,
+ actionTypeRegistry,
+ isESOCanEncrypt: true,
+ preconfiguredActions: [],
+ });
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: '123',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'mock-action',
+ },
+ references: [],
+ });
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: '234',
+ type: 'action_task_params',
+ attributes: {},
+ references: [],
+ });
+ await executeFn(savedObjectsClient, {
+ id: '123',
+ params: { baz: false },
+ spaceId: 'default',
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ source: asHttpRequestExecutionSource(request),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ });
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ 'action_task_params',
+ {
+ actionId: '123',
+ params: { baz: false },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ },
+ {}
+ );
+ });
+
test('schedules the action with all given parameters with a preconfigured action', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts
index 4f3ffbef36c6e..7dcd66c711bdd 100644
--- a/x-pack/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.ts
@@ -11,6 +11,7 @@ import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './ty
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects';
import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor';
import { isSavedObjectExecutionSource } from './lib';
+import { RelatedSavedObjects } from './lib/related_saved_objects';
interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
@@ -23,6 +24,7 @@ export interface ExecuteOptions extends Pick = {
if (
customHostSettings.find(
(customHostSchema: CustomHostSettings) =>
- !!customHostSchema.tls && !!customHostSchema.tls.rejectUnauthorized
+ !!customHostSchema.ssl && !!customHostSchema.ssl.rejectUnauthorized
)
) {
addDeprecation({
message:
- `"xpack.actions.customHostSettings[].tls.rejectUnauthorized" is deprecated.` +
- `Use "xpack.actions.customHostSettings[].tls.verificationMode" instead, ` +
+ `"xpack.actions.customHostSettings[].ssl.rejectUnauthorized" is deprecated.` +
+ `Use "xpack.actions.customHostSettings[].ssl.verificationMode" instead, ` +
`with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` +
`and "verificationMode:none" eql to "rejectUnauthorized:false".`,
correctiveActions: {
manualSteps: [
- `Remove "xpack.actions.customHostSettings[].tls.rejectUnauthorized" from your kibana configs.`,
- `Use "xpack.actions.customHostSettings[].tls.verificationMode" ` +
+ `Remove "xpack.actions.customHostSettings[].ssl.rejectUnauthorized" from your kibana configs.`,
+ `Use "xpack.actions.customHostSettings[].ssl.verificationMode" ` +
`with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` +
`and "verificationMode:none" eql to "rejectUnauthorized:false".`,
],
diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts
index 8ec94c4d4a552..37d461d6b2a50 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.test.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts
@@ -23,6 +23,7 @@ const services = actionsMock.createServices();
const actionsClient = actionsClientMock.create();
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
const actionTypeRegistry = actionTypeRegistryMock.create();
+const eventLogger = eventLoggerMock.create();
const executeParams = {
actionId: '1',
@@ -42,7 +43,7 @@ actionExecutor.initialize({
getActionsClientWithRequest,
actionTypeRegistry,
encryptedSavedObjectsClient,
- eventLogger: eventLoggerMock.create(),
+ eventLogger,
preconfiguredActions: [],
});
@@ -379,6 +380,50 @@ test('logs a warning when alert executor returns invalid status', async () => {
);
});
+test('writes to event log for execute and execute start', async () => {
+ const executorMock = setupActionExecutorMock();
+ executorMock.mockResolvedValue({
+ actionId: '1',
+ status: 'ok',
+ });
+ await actionExecutor.execute(executeParams);
+ expect(eventLogger.logEvent).toHaveBeenCalledTimes(2);
+ expect(eventLogger.logEvent.mock.calls[0][0]).toMatchObject({
+ event: {
+ action: 'execute-start',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action started: test:1: action-1',
+ });
+ expect(eventLogger.logEvent.mock.calls[1][0]).toMatchObject({
+ event: {
+ action: 'execute',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action executed: test:1: action-1',
+ });
+});
+
function setupActionExecutorMock() {
const actionType: jest.Mocked = {
id: 'test',
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index 0737e0ce3f071..e9e7b17288611 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -7,6 +7,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger, KibanaRequest } from 'src/core/server';
+import { cloneDeep } from 'lodash';
import { withSpan } from '@kbn/apm-utils';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
import {
@@ -22,6 +23,7 @@ import { EVENT_LOG_ACTIONS } from '../constants/event_log';
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { ActionsClient } from '../actions_client';
import { ActionExecutionSource } from './action_execution_source';
+import { RelatedSavedObjects } from './related_saved_objects';
export interface ActionExecutorContext {
logger: Logger;
@@ -42,6 +44,7 @@ export interface ExecuteOptions {
request: KibanaRequest;
params: Record;
source?: ActionExecutionSource;
+ relatedSavedObjects?: RelatedSavedObjects;
}
export type ActionExecutorContract = PublicMethodsOf;
@@ -68,6 +71,7 @@ export class ActionExecutor {
params,
request,
source,
+ relatedSavedObjects,
}: ExecuteOptions): Promise> {
if (!this.isInitialized) {
throw new Error('ActionExecutor not initialized');
@@ -154,7 +158,28 @@ export class ActionExecutor {
},
};
+ for (const relatedSavedObject of relatedSavedObjects || []) {
+ event.kibana?.saved_objects?.push({
+ rel: SAVED_OBJECT_REL_PRIMARY,
+ type: relatedSavedObject.type,
+ id: relatedSavedObject.id,
+ type_id: relatedSavedObject.typeId,
+ namespace: relatedSavedObject.namespace,
+ });
+ }
+
eventLogger.startTiming(event);
+
+ const startEvent = cloneDeep({
+ ...event,
+ event: {
+ ...event.event,
+ action: EVENT_LOG_ACTIONS.executeStart,
+ },
+ message: `action started: ${actionLabel}`,
+ });
+ eventLogger.logEvent(startEvent);
+
let rawResult: ActionTypeExecutorResult;
try {
rawResult = await actionType.executor({
diff --git a/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts b/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts
index ad07ea21d7917..ec7b46e545112 100644
--- a/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts
+++ b/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts
@@ -112,14 +112,14 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://elastic.co:443',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'xyz',
rejectUnauthorized: false,
},
},
{
url: 'smtp://mail.elastic.com:25',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'abc',
rejectUnauthorized: true,
},
@@ -338,7 +338,7 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://almost.purrfect.com/',
- tls: {
+ ssl: {
certificateAuthoritiesFiles: 'this-file-does-not-exist',
},
},
@@ -350,7 +350,7 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://almost.purrfect.com:443',
- tls: {
+ ssl: {
certificateAuthoritiesFiles: 'this-file-does-not-exist',
},
},
@@ -371,7 +371,7 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://almost.purrfect.com/',
- tls: {
+ ssl: {
certificateAuthoritiesFiles: CA_FILE1,
},
},
@@ -380,7 +380,7 @@ describe('custom_host_settings', () => {
const resConfig = resolveCustomHosts(mockLogger, config);
// not checking the full structure anymore, just ca bits
- expect(resConfig?.customHostSettings?.[0].tls?.certificateAuthoritiesData).toBe(CA_CONTENTS1);
+ expect(resConfig?.customHostSettings?.[0].ssl?.certificateAuthoritiesData).toBe(CA_CONTENTS1);
expect(warningLogs()).toEqual([]);
});
@@ -390,7 +390,7 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://almost.purrfect.com/',
- tls: {
+ ssl: {
certificateAuthoritiesFiles: [CA_FILE1, CA_FILE2],
},
},
@@ -399,7 +399,7 @@ describe('custom_host_settings', () => {
const resConfig = resolveCustomHosts(mockLogger, config);
// not checking the full structure anymore, just ca bits
- expect(resConfig?.customHostSettings?.[0].tls?.certificateAuthoritiesData).toBe(
+ expect(resConfig?.customHostSettings?.[0].ssl?.certificateAuthoritiesData).toBe(
`${CA_CONTENTS1}\n${CA_CONTENTS2}`
);
expect(warningLogs()).toEqual([]);
@@ -411,7 +411,7 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://almost.purrfect.com/',
- tls: {
+ ssl: {
certificateAuthoritiesFiles: [CA_FILE2],
certificateAuthoritiesData: CA_CONTENTS1,
},
@@ -421,7 +421,7 @@ describe('custom_host_settings', () => {
const resConfig = resolveCustomHosts(mockLogger, config);
// not checking the full structure anymore, just ca bits
- expect(resConfig?.customHostSettings?.[0].tls?.certificateAuthoritiesData).toBe(
+ expect(resConfig?.customHostSettings?.[0].ssl?.certificateAuthoritiesData).toBe(
`${CA_CONTENTS1}\n${CA_CONTENTS2}`
);
expect(warningLogs()).toEqual([]);
@@ -468,13 +468,13 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://almost.purrfect.com/',
- tls: {
+ ssl: {
rejectUnauthorized: true,
},
},
{
url: 'https://almost.purrfect.com:443',
- tls: {
+ ssl: {
rejectUnauthorized: false,
},
},
@@ -486,7 +486,7 @@ describe('custom_host_settings', () => {
customHostSettings: [
{
url: 'https://almost.purrfect.com:443',
- tls: {
+ ssl: {
rejectUnauthorized: true,
},
},
diff --git a/x-pack/plugins/actions/server/lib/custom_host_settings.ts b/x-pack/plugins/actions/server/lib/custom_host_settings.ts
index bfc8dad48aab6..0ff8624d42cfe 100644
--- a/x-pack/plugins/actions/server/lib/custom_host_settings.ts
+++ b/x-pack/plugins/actions/server/lib/custom_host_settings.ts
@@ -86,8 +86,8 @@ export function resolveCustomHosts(logger: Logger, config: ActionsConfig): Actio
}
// read the specified ca files, add their content to certificateAuthoritiesData
- if (customHostSetting.tls) {
- let files = customHostSetting.tls?.certificateAuthoritiesFiles || [];
+ if (customHostSetting.ssl) {
+ let files = customHostSetting.ssl?.certificateAuthoritiesFiles || [];
if (typeof files === 'string') {
files = [files];
}
@@ -134,12 +134,12 @@ export function resolveCustomHosts(logger: Logger, config: ActionsConfig): Actio
}
function appendToCertificateAuthoritiesData(customHost: CustomHostSettingsWriteable, cert: string) {
- const tls = customHost.tls;
- if (tls) {
- if (!tls.certificateAuthoritiesData) {
- tls.certificateAuthoritiesData = cert;
+ const ssl = customHost.ssl;
+ if (ssl) {
+ if (!ssl.certificateAuthoritiesData) {
+ ssl.certificateAuthoritiesData = cert;
} else {
- tls.certificateAuthoritiesData += '\n' + cert;
+ ssl.certificateAuthoritiesData += '\n' + cert;
}
}
}
diff --git a/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts
new file mode 100644
index 0000000000000..8fd13d1375697
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/related_saved_objects.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { validatedRelatedSavedObjects } from './related_saved_objects';
+import { loggingSystemMock } from '../../../../../src/core/server/mocks';
+import { Logger } from '../../../../../src/core/server';
+
+const loggerMock = loggingSystemMock.createLogger();
+
+describe('related_saved_objects', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('validates valid objects', () => {
+ ensureValid(loggerMock, undefined);
+ ensureValid(loggerMock, []);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ typeId: 'some-type-id',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ typeId: 'some-type-id',
+ namespace: 'some-namespace',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ },
+ {
+ id: 'some-id-2',
+ type: 'some-type-2',
+ },
+ ]);
+ });
+});
+
+it('handles invalid objects', () => {
+ ensureInvalid(loggerMock, 42);
+ ensureInvalid(loggerMock, {});
+ ensureInvalid(loggerMock, [{}]);
+ ensureInvalid(loggerMock, [{ id: 'some-id' }]);
+ ensureInvalid(loggerMock, [{ id: 42 }]);
+ ensureInvalid(loggerMock, [{ id: 'some-id', type: 'some-type', x: 42 }]);
+});
+
+function ensureValid(logger: Logger, savedObjects: unknown) {
+ const result = validatedRelatedSavedObjects(logger, savedObjects);
+ expect(result).toEqual(savedObjects === undefined ? [] : savedObjects);
+ expect(loggerMock.warn).not.toHaveBeenCalled();
+}
+
+function ensureInvalid(logger: Logger, savedObjects: unknown) {
+ const result = validatedRelatedSavedObjects(logger, savedObjects);
+ expect(result).toEqual([]);
+
+ const message = loggerMock.warn.mock.calls[0][0];
+ expect(message).toMatch(
+ /ignoring invalid related saved objects: expected value of type \[array\] but got/
+ );
+}
diff --git a/x-pack/plugins/actions/server/lib/related_saved_objects.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.ts
new file mode 100644
index 0000000000000..160587a3a9a8b
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/related_saved_objects.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { schema, TypeOf } from '@kbn/config-schema';
+import { Logger } from '../../../../../src/core/server';
+
+export type RelatedSavedObjects = TypeOf;
+
+const RelatedSavedObjectsSchema = schema.arrayOf(
+ schema.object({
+ namespace: schema.maybe(schema.string({ minLength: 1 })),
+ id: schema.string({ minLength: 1 }),
+ type: schema.string({ minLength: 1 }),
+ // optional; for SO types like action/alert that have type id's
+ typeId: schema.maybe(schema.string({ minLength: 1 })),
+ }),
+ { defaultValue: [] }
+);
+
+export function validatedRelatedSavedObjects(logger: Logger, data: unknown): RelatedSavedObjects {
+ try {
+ return RelatedSavedObjectsSchema.validate(data);
+ } catch (err) {
+ logger.warn(`ignoring invalid related saved objects: ${err.message}`);
+ return [];
+ }
+}
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
index 229324c1f0df3..2292994e3ccfd 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
@@ -126,6 +126,7 @@ test('executes the task by calling the executor with proper parameters', async (
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
+ relatedSavedObjects: [],
request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
@@ -247,6 +248,7 @@ test('uses API key when provided', async () => {
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
+ relatedSavedObjects: [],
request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
@@ -262,6 +264,79 @@ test('uses API key when provided', async () => {
);
});
+test('uses relatedSavedObjects when provided', async () => {
+ const taskRunner = taskRunnerFactory.create({
+ taskInstance: mockedTaskInstance,
+ });
+
+ mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
+ spaceIdToNamespace.mockReturnValueOnce('namespace-test');
+ mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
+ id: '3',
+ type: 'action_task_params',
+ attributes: {
+ actionId: '2',
+ params: { baz: true },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [{ id: 'some-id', type: 'some-type' }],
+ },
+ references: [],
+ });
+
+ await taskRunner.run();
+
+ expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
+ actionId: '2',
+ params: { baz: true },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ },
+ ],
+ request: expect.objectContaining({
+ headers: {
+ // base64 encoded "123:abc"
+ authorization: 'ApiKey MTIzOmFiYw==',
+ },
+ }),
+ });
+});
+
+test('sanitizes invalid relatedSavedObjects when provided', async () => {
+ const taskRunner = taskRunnerFactory.create({
+ taskInstance: mockedTaskInstance,
+ });
+
+ mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
+ spaceIdToNamespace.mockReturnValueOnce('namespace-test');
+ mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
+ id: '3',
+ type: 'action_task_params',
+ attributes: {
+ actionId: '2',
+ params: { baz: true },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [{ Xid: 'some-id', type: 'some-type' }],
+ },
+ references: [],
+ });
+
+ await taskRunner.run();
+
+ expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
+ actionId: '2',
+ params: { baz: true },
+ relatedSavedObjects: [],
+ request: expect.objectContaining({
+ headers: {
+ // base64 encoded "123:abc"
+ authorization: 'ApiKey MTIzOmFiYw==',
+ },
+ }),
+ });
+});
+
test(`doesn't use API key when not provided`, async () => {
const factory = new TaskRunnerFactory(mockedActionExecutor);
factory.initialize(taskRunnerFactoryInitializerParams);
@@ -284,6 +359,7 @@ test(`doesn't use API key when not provided`, async () => {
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
+ relatedSavedObjects: [],
request: expect.objectContaining({
headers: {},
}),
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 cf4b1576f2778..0515963ab82f4 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
@@ -30,6 +30,7 @@ import {
} from '../types';
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../constants/saved_objects';
import { asSavedObjectExecutionSource } from './action_execution_source';
+import { validatedRelatedSavedObjects } from './related_saved_objects';
export interface TaskRunnerContext {
logger: Logger;
@@ -77,7 +78,7 @@ export class TaskRunnerFactory {
const namespace = spaceIdToNamespace(spaceId);
const {
- attributes: { actionId, params, apiKey },
+ attributes: { actionId, params, apiKey, relatedSavedObjects },
references,
} = await encryptedSavedObjectsClient.getDecryptedAsInternalUser(
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
@@ -117,6 +118,7 @@ export class TaskRunnerFactory {
actionId,
request: fakeRequest,
...getSourceFromReferences(references),
+ relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects),
});
} catch (e) {
if (e instanceof ActionTypeDisabledError) {
diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts
index 4b12bf3111c1f..54e10698e5af9 100644
--- a/x-pack/plugins/actions/server/routes/execute.test.ts
+++ b/x-pack/plugins/actions/server/routes/execute.test.ts
@@ -65,6 +65,7 @@ describe('executeActionRoute', () => {
someData: 'data',
},
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
expect(res.ok).toHaveBeenCalled();
@@ -101,6 +102,7 @@ describe('executeActionRoute', () => {
expect(actionsClient.execute).toHaveBeenCalledWith({
actionId: '1',
params: {},
+ relatedSavedObjects: [],
source: asHttpRequestExecutionSource(req),
});
diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts
index 377fe1215b3fb..7e8110365e87a 100644
--- a/x-pack/plugins/actions/server/routes/execute.ts
+++ b/x-pack/plugins/actions/server/routes/execute.ts
@@ -53,6 +53,7 @@ export const executeActionRoute = (
params,
actionId: id,
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
return body
? res.ok({
diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
index 2ac53ddaaedf6..05b71819911a3 100644
--- a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
+++ b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
@@ -63,6 +63,7 @@ describe('executeActionRoute', () => {
someData: 'data',
},
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
expect(res.ok).toHaveBeenCalled();
@@ -100,6 +101,7 @@ describe('executeActionRoute', () => {
actionId: '1',
params: {},
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
expect(res.ok).not.toHaveBeenCalled();
diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.ts b/x-pack/plugins/actions/server/routes/legacy/execute.ts
index f6ddec1d01c20..d7ed8d2e15604 100644
--- a/x-pack/plugins/actions/server/routes/legacy/execute.ts
+++ b/x-pack/plugins/actions/server/routes/legacy/execute.ts
@@ -48,6 +48,7 @@ export const executeActionRoute = (
params,
actionId: id,
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
return body
? res.ok({
diff --git a/x-pack/plugins/actions/server/saved_objects/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json
index c598b96ba2451..57f801ae9a075 100644
--- a/x-pack/plugins/actions/server/saved_objects/mappings.json
+++ b/x-pack/plugins/actions/server/saved_objects/mappings.json
@@ -35,6 +35,10 @@
},
"apiKey": {
"type": "binary"
+ },
+ "relatedSavedObjects": {
+ "enabled": false,
+ "type": "object"
}
}
}
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index c8c9967afca1a..7c05d16923b9d 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -22,7 +22,7 @@ export { ActionTypeExecutorResult } from '../common';
export { GetFieldsByIssueTypeResponse as JiraGetFieldsResponse } from './builtin_action_types/jira/types';
export { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './builtin_action_types/servicenow/types';
export { GetCommonFieldsResponse as ResilientGetFieldsResponse } from './builtin_action_types/resilient/types';
-
+export { SwimlanePublicConfigurationType } from './builtin_action_types/swimlane/types';
export type WithoutQueryAndParams = Pick>;
export type GetServicesFunction = (request: KibanaRequest) => Services;
export type ActionTypeRegistryContract = PublicMethodsOf;
@@ -142,7 +142,7 @@ export interface ProxySettings {
proxyBypassHosts: Set | undefined;
proxyOnlyHosts: Set | undefined;
proxyHeaders?: Record;
- proxyTLSSettings: TLSSettings;
+ proxySSLSettings: SSLSettings;
}
export interface ResponseSettings {
@@ -150,6 +150,6 @@ export interface ResponseSettings {
timeout: number;
}
-export interface TLSSettings {
+export interface SSLSettings {
verificationMode?: 'none' | 'certificate' | 'full';
}
diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts
index 06248e1fa95a8..80e0c19092c78 100644
--- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts
+++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts
@@ -18,6 +18,7 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = {
__email: { type: 'long' },
__index: { type: 'long' },
__pagerduty: { type: 'long' },
+ __swimlane: { type: 'long' },
'__server-log': { type: 'long' },
__slack: { type: 'long' },
__webhook: { type: 'long' },
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
index 25f0656163f5d..033ffcceb6a0a 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
@@ -135,6 +135,14 @@ test('enqueues execution per selected action', async () => {
"foo": true,
"stateVal": "My goes here",
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": "test1",
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -247,6 +255,14 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () =>
id: '1',
type: 'alert',
}),
+ relatedSavedObjects: [
+ {
+ id: '1',
+ namespace: 'test1',
+ type: 'alert',
+ typeId: 'test',
+ },
+ ],
spaceId: 'test1',
apiKey: createExecutionHandlerParams.apiKey,
});
@@ -327,6 +343,14 @@ test('context attribute gets parameterized', async () => {
"foo": true,
"stateVal": "My goes here",
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": "test1",
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -360,6 +384,14 @@ test('state attribute gets parameterized', async () => {
"foo": true,
"stateVal": "My state-val goes here",
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": "test1",
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
index c3a36297c217a..968fff540dc03 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
@@ -157,6 +157,8 @@ export function createExecutionHandler<
continue;
}
+ const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
+
// TODO would be nice to add the action name here, but it's not available
const actionLabel = `${action.actionTypeId}:${action.id}`;
const actionsClient = await actionsPlugin.getActionsClientWithRequest(request);
@@ -169,10 +171,16 @@ export function createExecutionHandler<
id: alertId,
type: 'alert',
}),
+ relatedSavedObjects: [
+ {
+ id: alertId,
+ type: 'alert',
+ namespace: namespace.namespace,
+ typeId: alertType.id,
+ },
+ ],
});
- const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
-
const event: IEvent = {
event: {
action: EVENT_LOG_ACTIONS.executeAction,
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
index 39a45584631d2..8ab267a5610d3 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -352,6 +352,14 @@ describe('Task Runner', () => {
"params": Object {
"foo": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -1098,6 +1106,14 @@ describe('Task Runner', () => {
"params": Object {
"foo": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -1634,6 +1650,14 @@ describe('Task Runner', () => {
"params": Object {
"isResolved": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -1826,6 +1850,14 @@ describe('Task Runner', () => {
"params": Object {
"isResolved": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx
new file mode 100644
index 0000000000000..8cc16dd801c25
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { ComponentType } from 'react';
+import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
+import {
+ ApmPluginContext,
+ ApmPluginContextValue,
+} from '../../../../context/apm_plugin/apm_plugin_context';
+import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
+import { ErrorDistribution } from './';
+
+export default {
+ title: 'app/ErrorGroupDetails/Distribution',
+ component: ErrorDistribution,
+ decorators: [
+ (Story: ComponentType) => {
+ const apmPluginContextMock = ({
+ observabilityRuleTypeRegistry: { getFormatter: () => undefined },
+ } as unknown) as ApmPluginContextValue;
+
+ const kibanaContextServices = {
+ uiSettings: { get: () => {} },
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+ },
+ ],
+};
+
+export function Example() {
+ const distribution = {
+ noHits: false,
+ bucketSize: 62350,
+ buckets: [
+ { key: 1624279912350, count: 6 },
+ { key: 1624279974700, count: 1 },
+ { key: 1624280037050, count: 2 },
+ { key: 1624280099400, count: 3 },
+ { key: 1624280161750, count: 13 },
+ { key: 1624280224100, count: 1 },
+ { key: 1624280286450, count: 2 },
+ { key: 1624280348800, count: 0 },
+ { key: 1624280411150, count: 4 },
+ { key: 1624280473500, count: 4 },
+ { key: 1624280535850, count: 1 },
+ { key: 1624280598200, count: 4 },
+ { key: 1624280660550, count: 0 },
+ { key: 1624280722900, count: 2 },
+ { key: 1624280785250, count: 3 },
+ { key: 1624280847600, count: 0 },
+ ],
+ };
+
+ return ;
+}
+
+export function EmptyState() {
+ return (
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
index 643653c24aeb3..e53aaf97cdf75 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
@@ -67,6 +67,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
const xFormatter = niceTimeFormatter([xMin, xMax]);
const { observabilityRuleTypeRegistry } = useApmPluginContext();
+
const { alerts } = useApmServiceContext();
const { getFormatter } = observabilityRuleTypeRegistry;
const [selectedAlertId, setSelectedAlertId] = useState(
@@ -84,7 +85,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
};
return (
-
+ <>
{title}
@@ -124,7 +125,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
alerts: alerts?.filter(
(alert) => alert[RULE_ID]?.[0] === AlertType.ErrorCount
),
- chartStartTime: buckets[0].x0,
+ chartStartTime: buckets[0]?.x0,
getFormatter,
selectedAlertId,
setSelectedAlertId,
@@ -143,6 +144,6 @@ export function ErrorDistribution({ distribution, title }: Props) {
-
+ >
);
}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
index 20d930d28599f..63ba7047696ca 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx
@@ -47,10 +47,11 @@ export function UXActionMenu({
const uxExploratoryViewLink = createExploratoryViewUrl(
{
- 'ux-series': {
+ 'ux-series': ({
dataType: 'ux',
+ isNew: true,
time: { from: rangeFrom, to: rangeTo },
- } as SeriesUrl,
+ } as unknown) as SeriesUrl,
},
http?.basePath.get()
);
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
index e0486af6cd6ef..5c63cc24b6fdf 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
@@ -89,7 +89,7 @@ export function PageLoadDistribution() {
{
[`${serviceName}-page-views`]: {
dataType: 'ux',
- reportType: 'dist',
+ reportType: 'data-distribution',
time: { from: rangeFrom!, to: rangeTo! },
reportDefinitions: {
'service.name': serviceName as string[],
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
index c45637e5d3c82..667d0b5e4b4db 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
@@ -64,7 +64,7 @@ export function PageViewsTrend() {
{
[`${serviceName}-page-views`]: {
dataType: 'ux',
- reportType: 'kpi',
+ reportType: 'kpi-over-time',
time: { from: rangeFrom!, to: rangeTo! },
reportDefinitions: {
'service.name': serviceName as string[],
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx
index 8549f09bba248..09fbf07b8ecbd 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx
@@ -5,10 +5,21 @@
* 2.0.
*/
+import { ReactNode } from 'react';
+import { StyledComponent } from 'styled-components';
import { EuiFlyout } from '@elastic/eui';
-import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
+import {
+ euiStyled,
+ EuiTheme,
+} from '../../../../../../../../../../src/plugins/kibana_react/common';
-export const ResponsiveFlyout = euiStyled(EuiFlyout)`
+// TODO: EUI team follow up on complex types and styled-components `styled`
+// https://github.com/elastic/eui/issues/4855
+export const ResponsiveFlyout: StyledComponent<
+ typeof EuiFlyout,
+ EuiTheme,
+ { children?: ReactNode }
+> = euiStyled(EuiFlyout)`
width: 100%;
@media (min-width: 800px) {
diff --git a/x-pack/plugins/canvas/CONTRIBUTING.md b/x-pack/plugins/canvas/CONTRIBUTING.md
index d3bff67771244..d8a657ea73c40 100644
--- a/x-pack/plugins/canvas/CONTRIBUTING.md
+++ b/x-pack/plugins/canvas/CONTRIBUTING.md
@@ -36,8 +36,8 @@ To keep the code terse, Canvas uses i18n "dictionaries": abstracted, static sing
```js
-// i18n/components.ts
-export const ComponentStrings = {
+// asset_manager.tsx
+const strings = {
// ...
AssetManager: {
getCopyAssetMessage: (id: string) =>
@@ -52,10 +52,6 @@ export const ComponentStrings = {
// ...
};
-// asset_manager.tsx
-import { ComponentStrings } from '../../../i18n';
-const { AssetManager: strings } = ComponentStrings;
-
const text = (
{strings.getSpaceUsedText(percentageUsed)}
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx
index 4dfb4c3f09273..b5c009abc2768 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx
@@ -5,12 +5,22 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
-import { ComponentStrings } from '../../../../../i18n';
+import PropTypes from 'prop-types';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-const { AdvancedFilter: strings } = ComponentStrings;
+const strings = {
+ getApplyButtonLabel: () =>
+ i18n.translate('xpack.canvas.renderer.advancedFilter.applyButtonLabel', {
+ defaultMessage: 'Apply',
+ description: 'This refers to applying the filter to the Canvas workpad',
+ }),
+ getInputPlaceholder: () =>
+ i18n.translate('xpack.canvas.renderer.advancedFilter.inputPlaceholder', {
+ defaultMessage: 'Enter filter expression',
+ }),
+};
export interface Props {
/** Optional value for the component */
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx
index 86517c897f02d..43f2e1ecc84f3 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx
@@ -5,12 +5,18 @@
* 2.0.
*/
-import { EuiIcon } from '@elastic/eui';
-import PropTypes from 'prop-types';
import React, { ChangeEvent, FocusEvent, FunctionComponent } from 'react';
-import { ComponentStrings } from '../../../../../i18n';
+import PropTypes from 'prop-types';
+import { EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-const { DropdownFilter: strings } = ComponentStrings;
+const strings = {
+ getMatchAllOptionLabel: () =>
+ i18n.translate('xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel', {
+ defaultMessage: 'ANY',
+ description: 'The dropdown filter option to match any value in the field.',
+ }),
+};
export interface Props {
/**
diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts
deleted file mode 100644
index 7a23137e7ef60..0000000000000
--- a/x-pack/plugins/canvas/i18n/components.ts
+++ /dev/null
@@ -1,1764 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-import { BOLD_MD_TOKEN, CANVAS, HTML, JSON, PDF, URL, ZIP } from './constants';
-
-export const ComponentStrings = {
- AddEmbeddableFlyout: {
- getNoItemsText: () =>
- i18n.translate('xpack.canvas.embedObject.noMatchingObjectsMessage', {
- defaultMessage: 'No matching objects found.',
- }),
- getTitleText: () =>
- i18n.translate('xpack.canvas.embedObject.titleText', {
- defaultMessage: 'Add from Kibana',
- }),
- },
- AdvancedFilter: {
- getApplyButtonLabel: () =>
- i18n.translate('xpack.canvas.renderer.advancedFilter.applyButtonLabel', {
- defaultMessage: 'Apply',
- description: 'This refers to applying the filter to the Canvas workpad',
- }),
- getInputPlaceholder: () =>
- i18n.translate('xpack.canvas.renderer.advancedFilter.inputPlaceholder', {
- defaultMessage: 'Enter filter expression',
- }),
- },
- App: {
- getLoadErrorMessage: (error: string) =>
- i18n.translate('xpack.canvas.app.loadErrorMessage', {
- defaultMessage: 'Message: {error}',
- values: {
- error,
- },
- }),
- getLoadErrorTitle: () =>
- i18n.translate('xpack.canvas.app.loadErrorTitle', {
- defaultMessage: 'Canvas failed to load',
- }),
- getLoadingMessage: () =>
- i18n.translate('xpack.canvas.app.loadingMessage', {
- defaultMessage: 'Canvas is loading',
- }),
- },
- ArgAddPopover: {
- getAddAriaLabel: () =>
- i18n.translate('xpack.canvas.argAddPopover.addAriaLabel', {
- defaultMessage: 'Add argument',
- }),
- },
- ArgFormAdvancedFailure: {
- getApplyButtonLabel: () =>
- i18n.translate('xpack.canvas.argFormAdvancedFailure.applyButtonLabel', {
- defaultMessage: 'Apply',
- }),
- getResetButtonLabel: () =>
- i18n.translate('xpack.canvas.argFormAdvancedFailure.resetButtonLabel', {
- defaultMessage: 'Reset',
- }),
- getRowErrorMessage: () =>
- i18n.translate('xpack.canvas.argFormAdvancedFailure.rowErrorMessage', {
- defaultMessage: 'Invalid Expression',
- }),
- },
- ArgFormArgSimpleForm: {
- getRemoveAriaLabel: () =>
- i18n.translate('xpack.canvas.argFormArgSimpleForm.removeAriaLabel', {
- defaultMessage: 'Remove',
- }),
- getRequiredTooltip: () =>
- i18n.translate('xpack.canvas.argFormArgSimpleForm.requiredTooltip', {
- defaultMessage: 'This argument is required, you should specify a value.',
- }),
- },
- ArgFormPendingArgValue: {
- getLoadingMessage: () =>
- i18n.translate('xpack.canvas.argFormPendingArgValue.loadingMessage', {
- defaultMessage: 'Loading',
- }),
- },
- ArgFormSimpleFailure: {
- getFailureTooltip: () =>
- i18n.translate('xpack.canvas.argFormSimpleFailure.failureTooltip', {
- defaultMessage:
- 'The interface for this argument could not parse the value, so a fallback input is being used',
- }),
- },
- Asset: {
- getCopyAssetTooltip: () =>
- i18n.translate('xpack.canvas.asset.copyAssetTooltip', {
- defaultMessage: 'Copy id to clipboard',
- }),
- getCreateImageTooltip: () =>
- i18n.translate('xpack.canvas.asset.createImageTooltip', {
- defaultMessage: 'Create image element',
- }),
- getDeleteAssetTooltip: () =>
- i18n.translate('xpack.canvas.asset.deleteAssetTooltip', {
- defaultMessage: 'Delete',
- }),
- getDownloadAssetTooltip: () =>
- i18n.translate('xpack.canvas.asset.downloadAssetTooltip', {
- defaultMessage: 'Download',
- }),
- getThumbnailAltText: () =>
- i18n.translate('xpack.canvas.asset.thumbnailAltText', {
- defaultMessage: 'Asset thumbnail',
- }),
- getConfirmModalButtonLabel: () =>
- i18n.translate('xpack.canvas.asset.confirmModalButtonLabel', {
- defaultMessage: 'Remove',
- }),
- getConfirmModalMessageText: () =>
- i18n.translate('xpack.canvas.asset.confirmModalDetail', {
- defaultMessage: 'Are you sure you want to remove this asset?',
- }),
- getConfirmModalTitle: () =>
- i18n.translate('xpack.canvas.asset.confirmModalTitle', {
- defaultMessage: 'Remove Asset',
- }),
- },
- AssetManager: {
- getButtonLabel: () =>
- i18n.translate('xpack.canvas.assetManager.manageButtonLabel', {
- defaultMessage: 'Manage assets',
- }),
- getDescription: () =>
- i18n.translate('xpack.canvas.assetModal.modalDescription', {
- defaultMessage:
- '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.',
- }),
- getEmptyAssetsDescription: () =>
- i18n.translate('xpack.canvas.assetModal.emptyAssetsDescription', {
- defaultMessage: 'Import your assets to get started',
- }),
- getFilePickerPromptText: () =>
- i18n.translate('xpack.canvas.assetModal.filePickerPromptText', {
- defaultMessage: 'Select or drag and drop images',
- }),
- getLoadingText: () =>
- i18n.translate('xpack.canvas.assetModal.loadingText', {
- defaultMessage: 'Uploading images',
- }),
- getModalCloseButtonLabel: () =>
- i18n.translate('xpack.canvas.assetModal.modalCloseButtonLabel', {
- defaultMessage: 'Close',
- }),
- getModalTitle: () =>
- i18n.translate('xpack.canvas.assetModal.modalTitle', {
- defaultMessage: 'Manage workpad assets',
- }),
- getSpaceUsedText: (percentageUsed: number) =>
- i18n.translate('xpack.canvas.assetModal.spacedUsedText', {
- defaultMessage: '{percentageUsed}% space used',
- values: {
- percentageUsed,
- },
- }),
- getCopyAssetMessage: (id: string) =>
- i18n.translate('xpack.canvas.assetModal.copyAssetMessage', {
- defaultMessage: `Copied '{id}' to clipboard`,
- values: {
- id,
- },
- }),
- },
- AssetPicker: {
- getAssetAltText: () =>
- i18n.translate('xpack.canvas.assetpicker.assetAltText', {
- defaultMessage: 'Asset thumbnail',
- }),
- },
- CanvasLoading: {
- getLoadingLabel: () =>
- i18n.translate('xpack.canvas.canvasLoading.loadingMessage', {
- defaultMessage: 'Loading',
- }),
- },
- ColorManager: {
- getAddAriaLabel: () =>
- i18n.translate('xpack.canvas.colorManager.addAriaLabel', {
- defaultMessage: 'Add Color',
- }),
- getCodePlaceholder: () =>
- i18n.translate('xpack.canvas.colorManager.codePlaceholder', {
- defaultMessage: 'Color code',
- }),
- getRemoveAriaLabel: () =>
- i18n.translate('xpack.canvas.colorManager.removeAriaLabel', {
- defaultMessage: 'Remove Color',
- }),
- },
- CustomElementModal: {
- getCancelButtonLabel: () =>
- i18n.translate('xpack.canvas.customElementModal.cancelButtonLabel', {
- defaultMessage: 'Cancel',
- }),
- getCharactersRemainingDescription: (numberOfRemainingCharacter: number) =>
- i18n.translate('xpack.canvas.customElementModal.remainingCharactersDescription', {
- defaultMessage: '{numberOfRemainingCharacter} characters remaining',
- values: {
- numberOfRemainingCharacter,
- },
- }),
- getDescriptionInputLabel: () =>
- i18n.translate('xpack.canvas.customElementModal.descriptionInputLabel', {
- defaultMessage: 'Description',
- }),
- getElementPreviewTitle: () =>
- i18n.translate('xpack.canvas.customElementModal.elementPreviewTitle', {
- defaultMessage: 'Element preview',
- }),
- getImageFilePickerPlaceholder: () =>
- i18n.translate('xpack.canvas.customElementModal.imageFilePickerPlaceholder', {
- defaultMessage: 'Select or drag and drop an image',
- }),
- getImageInputDescription: () =>
- i18n.translate('xpack.canvas.customElementModal.imageInputDescription', {
- defaultMessage:
- 'Take a screenshot of your element and upload it here. This can also be done after saving.',
- }),
- getImageInputLabel: () =>
- i18n.translate('xpack.canvas.customElementModal.imageInputLabel', {
- defaultMessage: 'Thumbnail image',
- }),
- getNameInputLabel: () =>
- i18n.translate('xpack.canvas.customElementModal.nameInputLabel', {
- defaultMessage: 'Name',
- }),
- getSaveButtonLabel: () =>
- i18n.translate('xpack.canvas.customElementModal.saveButtonLabel', {
- defaultMessage: 'Save',
- }),
- },
- DatasourceDatasourceComponent: {
- getChangeButtonLabel: () =>
- i18n.translate('xpack.canvas.datasourceDatasourceComponent.changeButtonLabel', {
- defaultMessage: 'Change element data source',
- }),
- getExpressionArgDescription: () =>
- i18n.translate('xpack.canvas.datasourceDatasourceComponent.expressionArgDescription', {
- defaultMessage:
- 'The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.',
- }),
- getPreviewButtonLabel: () =>
- i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', {
- defaultMessage: 'Preview data',
- }),
- getSaveButtonLabel: () =>
- i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', {
- defaultMessage: 'Save',
- }),
- },
- DatasourceDatasourcePreview: {
- getEmptyFirstLineDescription: () =>
- i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyFirstLineDescription', {
- defaultMessage: "We couldn't find any documents matching your search criteria.",
- }),
- getEmptySecondLineDescription: () =>
- i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptySecondLineDescription', {
- defaultMessage: 'Check your datasource settings and try again.',
- }),
- getEmptyTitle: () =>
- i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyTitle', {
- defaultMessage: 'No documents found',
- }),
- getModalTitle: () =>
- i18n.translate('xpack.canvas.datasourceDatasourcePreview.modalTitle', {
- defaultMessage: 'Datasource preview',
- }),
- },
- DatasourceNoDatasource: {
- getPanelDescription: () =>
- i18n.translate('xpack.canvas.datasourceNoDatasource.panelDescription', {
- defaultMessage:
- "This element does not have an attached data source. This is usually because the element is an image or other static asset. If that's not the case you might want to check your expression to make sure it is not malformed.",
- }),
- getPanelTitle: () =>
- i18n.translate('xpack.canvas.datasourceNoDatasource.panelTitle', {
- defaultMessage: 'No data source present',
- }),
- },
- DropdownFilter: {
- getMatchAllOptionLabel: () =>
- i18n.translate('xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel', {
- defaultMessage: 'ANY',
- description: 'The dropdown filter option to match any value in the field.',
- }),
- },
- ElementConfig: {
- getFailedLabel: () =>
- i18n.translate('xpack.canvas.elementConfig.failedLabel', {
- defaultMessage: 'Failed',
- description:
- 'The label for the total number of elements in a workpad that have thrown an error or failed to load',
- }),
- getLoadedLabel: () =>
- i18n.translate('xpack.canvas.elementConfig.loadedLabel', {
- defaultMessage: 'Loaded',
- description: 'The label for the number of elements in a workpad that have loaded',
- }),
- getProgressLabel: () =>
- i18n.translate('xpack.canvas.elementConfig.progressLabel', {
- defaultMessage: 'Progress',
- description: 'The label for the percentage of elements that have finished loading',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.elementConfig.title', {
- defaultMessage: 'Element status',
- description:
- '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad',
- }),
- getTotalLabel: () =>
- i18n.translate('xpack.canvas.elementConfig.totalLabel', {
- defaultMessage: 'Total',
- description: 'The label for the total number of elements in a workpad',
- }),
- },
- ElementControls: {
- getDeleteAriaLabel: () =>
- i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', {
- defaultMessage: 'Delete element',
- }),
- getDeleteTooltip: () =>
- i18n.translate('xpack.canvas.elementControls.deleteToolTip', {
- defaultMessage: 'Delete',
- }),
- getEditAriaLabel: () =>
- i18n.translate('xpack.canvas.elementControls.editAriaLabel', {
- defaultMessage: 'Edit element',
- }),
- getEditTooltip: () =>
- i18n.translate('xpack.canvas.elementControls.editToolTip', {
- defaultMessage: 'Edit',
- }),
- },
- ElementSettings: {
- getDataTabLabel: () =>
- i18n.translate('xpack.canvas.elementSettings.dataTabLabel', {
- defaultMessage: 'Data',
- description:
- 'This tab contains the settings for the data (i.e. Elasticsearch query) used as ' +
- 'the source for a Canvas element',
- }),
- getDisplayTabLabel: () =>
- i18n.translate('xpack.canvas.elementSettings.displayTabLabel', {
- defaultMessage: 'Display',
- description: 'This tab contains the settings for how data is displayed in a Canvas element',
- }),
- },
- Error: {
- getDescription: () =>
- i18n.translate('xpack.canvas.errorComponent.description', {
- defaultMessage: 'Expression failed with the message:',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.errorComponent.title', {
- defaultMessage: 'Whoops! Expression failed',
- }),
- },
- Expression: {
- getCancelButtonLabel: () =>
- i18n.translate('xpack.canvas.expression.cancelButtonLabel', {
- defaultMessage: 'Cancel',
- }),
- getCloseButtonLabel: () =>
- i18n.translate('xpack.canvas.expression.closeButtonLabel', {
- defaultMessage: 'Close',
- }),
- getLearnLinkText: () =>
- i18n.translate('xpack.canvas.expression.learnLinkText', {
- defaultMessage: 'Learn expression syntax',
- }),
- getMaximizeButtonLabel: () =>
- i18n.translate('xpack.canvas.expression.maximizeButtonLabel', {
- defaultMessage: 'Maximize editor',
- }),
- getMinimizeButtonLabel: () =>
- i18n.translate('xpack.canvas.expression.minimizeButtonLabel', {
- defaultMessage: 'Minimize Editor',
- }),
- getRunButtonLabel: () =>
- i18n.translate('xpack.canvas.expression.runButtonLabel', {
- defaultMessage: 'Run',
- }),
- getRunTooltip: () =>
- i18n.translate('xpack.canvas.expression.runTooltip', {
- defaultMessage: 'Run the expression',
- }),
- },
- ExpressionElementNotSelected: {
- getCloseButtonLabel: () =>
- i18n.translate('xpack.canvas.expressionElementNotSelected.closeButtonLabel', {
- defaultMessage: 'Close',
- }),
- getSelectDescription: () =>
- i18n.translate('xpack.canvas.expressionElementNotSelected.selectDescription', {
- defaultMessage: 'Select an element to show expression input',
- }),
- },
- ExpressionInput: {
- getArgReferenceAliasesDetail: (aliases: string) =>
- i18n.translate('xpack.canvas.expressionInput.argReferenceAliasesDetail', {
- defaultMessage: '{BOLD_MD_TOKEN}Aliases{BOLD_MD_TOKEN}: {aliases}',
- values: {
- BOLD_MD_TOKEN,
- aliases,
- },
- }),
- getArgReferenceDefaultDetail: (defaultVal: string) =>
- i18n.translate('xpack.canvas.expressionInput.argReferenceDefaultDetail', {
- defaultMessage: '{BOLD_MD_TOKEN}Default{BOLD_MD_TOKEN}: {defaultVal}',
- values: {
- BOLD_MD_TOKEN,
- defaultVal,
- },
- }),
- getArgReferenceRequiredDetail: (required: string) =>
- i18n.translate('xpack.canvas.expressionInput.argReferenceRequiredDetail', {
- defaultMessage: '{BOLD_MD_TOKEN}Required{BOLD_MD_TOKEN}: {required}',
- values: {
- BOLD_MD_TOKEN,
- required,
- },
- }),
- getArgReferenceTypesDetail: (types: string) =>
- i18n.translate('xpack.canvas.expressionInput.argReferenceTypesDetail', {
- defaultMessage: '{BOLD_MD_TOKEN}Types{BOLD_MD_TOKEN}: {types}',
- values: {
- BOLD_MD_TOKEN,
- types,
- },
- }),
- getFunctionReferenceAcceptsDetail: (acceptTypes: string) =>
- i18n.translate('xpack.canvas.expressionInput.functionReferenceAccepts', {
- defaultMessage: '{BOLD_MD_TOKEN}Accepts{BOLD_MD_TOKEN}: {acceptTypes}',
- values: {
- BOLD_MD_TOKEN,
- acceptTypes,
- },
- }),
- getFunctionReferenceReturnsDetail: (returnType: string) =>
- i18n.translate('xpack.canvas.expressionInput.functionReferenceReturns', {
- defaultMessage: '{BOLD_MD_TOKEN}Returns{BOLD_MD_TOKEN}: {returnType}',
- values: {
- BOLD_MD_TOKEN,
- returnType,
- },
- }),
- },
- FunctionFormContextError: {
- getContextErrorMessage: (errorMessage: string) =>
- i18n.translate('xpack.canvas.functionForm.contextError', {
- defaultMessage: 'ERROR: {errorMessage}',
- values: {
- errorMessage,
- },
- }),
- },
- FunctionFormFunctionUnknown: {
- getUnknownArgumentTypeErrorMessage: (expressionType: string) =>
- i18n.translate('xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError', {
- defaultMessage: 'Unknown expression type "{expressionType}"',
- values: {
- expressionType,
- },
- }),
- },
- GroupSettings: {
- getSaveGroupDescription: () =>
- i18n.translate('xpack.canvas.groupSettings.saveGroupDescription', {
- defaultMessage: 'Save this group as a new element to re-use it throughout your workpad.',
- }),
- getUngroupDescription: () =>
- i18n.translate('xpack.canvas.groupSettings.ungroupDescription', {
- defaultMessage: 'Ungroup ({uKey}) to edit individual element settings.',
- values: {
- uKey: 'U',
- },
- }),
- },
- HelpMenu: {
- getDocumentationLinkLabel: () =>
- i18n.translate('xpack.canvas.helpMenu.documentationLinkLabel', {
- defaultMessage: '{CANVAS} documentation',
- values: {
- CANVAS,
- },
- }),
- getHelpMenuDescription: () =>
- i18n.translate('xpack.canvas.helpMenu.description', {
- defaultMessage: 'For {CANVAS} specific information',
- values: {
- CANVAS,
- },
- }),
- getKeyboardShortcutsLinkLabel: () =>
- i18n.translate('xpack.canvas.helpMenu.keyboardShortcutsLinkLabel', {
- defaultMessage: 'Keyboard shortcuts',
- }),
- },
- KeyboardShortcutsDoc: {
- getFlyoutCloseButtonAriaLabel: () =>
- i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel', {
- defaultMessage: 'Closes keyboard shortcuts reference',
- }),
- getShortcutSeparator: () =>
- i18n.translate('xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator', {
- defaultMessage: 'or',
- description:
- 'Separates which keyboard shortcuts can be used for a single action. Example: "{shortcut1} or {shortcut2} or {shortcut3}"',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle', {
- defaultMessage: 'Keyboard shortcuts',
- }),
- },
- LabsControl: {
- getLabsButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel', {
- defaultMessage: 'Labs',
- }),
- getAriaLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsAriaLabel', {
- defaultMessage: 'View labs projects',
- }),
- getTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsTooltip', {
- defaultMessage: 'View labs projects',
- }),
- },
- Link: {
- getErrorMessage: (message: string) =>
- i18n.translate('xpack.canvas.link.errorMessage', {
- defaultMessage: 'LINK ERROR: {message}',
- values: {
- message,
- },
- }),
- },
- MultiElementSettings: {
- getMultipleElementsActionsDescription: () =>
- i18n.translate('xpack.canvas.groupSettings.multipleElementsActionsDescription', {
- defaultMessage:
- 'Deselect these elements to edit their individual settings, press ({gKey}) to group them, or save this selection as a new ' +
- 'element to re-use it throughout your workpad.',
- values: {
- gKey: 'G',
- },
- }),
- getMultipleElementsDescription: () =>
- i18n.translate('xpack.canvas.groupSettings.multipleElementsDescription', {
- defaultMessage: 'Multiple elements are currently selected.',
- }),
- },
- PageConfig: {
- getBackgroundColorDescription: () =>
- i18n.translate('xpack.canvas.pageConfig.backgroundColorDescription', {
- defaultMessage: 'Accepts HEX, RGB or HTML color names',
- }),
- getBackgroundColorLabel: () =>
- i18n.translate('xpack.canvas.pageConfig.backgroundColorLabel', {
- defaultMessage: 'Background',
- }),
- getNoTransitionDropDownOptionLabel: () =>
- i18n.translate('xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel', {
- defaultMessage: 'None',
- description:
- 'This is the option the user should choose if they do not want any page transition (i.e. fade in, fade out, etc) to ' +
- 'be applied to the current page.',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.pageConfig.title', {
- defaultMessage: 'Page settings',
- }),
- getTransitionLabel: () =>
- i18n.translate('xpack.canvas.pageConfig.transitionLabel', {
- defaultMessage: 'Transition',
- description:
- 'This refers to the transition effect, such as fade in or rotate, applied to a page in presentation mode.',
- }),
- getTransitionPreviewLabel: () =>
- i18n.translate('xpack.canvas.pageConfig.transitionPreviewLabel', {
- defaultMessage: 'Preview',
- description: 'This is the label for a preview of the transition effect selected.',
- }),
- },
- PageManager: {
- getPageNumberAriaLabel: (pageNumber: number) =>
- i18n.translate('xpack.canvas.pageManager.pageNumberAriaLabel', {
- defaultMessage: 'Load page number {pageNumber}',
- values: {
- pageNumber,
- },
- }),
- getAddPageTooltip: () =>
- i18n.translate('xpack.canvas.pageManager.addPageTooltip', {
- defaultMessage: 'Add a new page to this workpad',
- }),
- getConfirmRemoveTitle: () =>
- i18n.translate('xpack.canvas.pageManager.confirmRemoveTitle', {
- defaultMessage: 'Remove Page',
- }),
- getConfirmRemoveDescription: () =>
- i18n.translate('xpack.canvas.pageManager.confirmRemoveDescription', {
- defaultMessage: 'Are you sure you want to remove this page?',
- }),
- getConfirmRemoveButtonLabel: () =>
- i18n.translate('xpack.canvas.pageManager.removeButtonLabel', {
- defaultMessage: 'Remove',
- }),
- },
- PagePreviewPageControls: {
- getClonePageAriaLabel: () =>
- i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageAriaLabel', {
- defaultMessage: 'Clone page',
- }),
- getClonePageTooltip: () =>
- i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageTooltip', {
- defaultMessage: 'Clone',
- }),
- getDeletePageAriaLabel: () =>
- i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageAriaLabel', {
- defaultMessage: 'Delete page',
- }),
- getDeletePageTooltip: () =>
- i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageTooltip', {
- defaultMessage: 'Delete',
- }),
- },
- PalettePicker: {
- getEmptyPaletteLabel: () =>
- i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', {
- defaultMessage: 'None',
- }),
- getNoPaletteFoundErrorTitle: () =>
- i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', {
- defaultMessage: 'Color palette not found',
- }),
- },
- 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', {
- defaultMessage: 'Download runtime',
- }),
- getSnippentsStepTitle: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.addSnippetsTitle', {
- defaultMessage: 'Add snippets to website',
- }),
- getStepsDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.description', {
- defaultMessage:
- 'Follow these steps to share a static version of this workpad on an external website. It will be a visual snapshot of the current workpad, and will not have access to live data.',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.flyoutTitle', {
- defaultMessage: 'Share on a website',
- }),
- getUnsupportedRendererWarning: () =>
- 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: {
- CANVAS,
- },
- }),
- getWorkpadStepTitle: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadWorkpadTitle', {
- defaultMessage: 'Download workpad',
- }),
- },
- ShareWebsiteRuntimeStep: {
- getDownloadLabel: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.downloadLabel', {
- defaultMessage: 'Download runtime',
- }),
- getStepDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.description', {
- defaultMessage:
- 'In order to render a Shareable Workpad, you also need to include the {CANVAS} Shareable Workpad Runtime. You can skip this step if the runtime is already included on your website.',
- values: {
- CANVAS,
- },
- }),
- },
- ShareWebsiteSnippetsStep: {
- getAutoplayParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.autoplayParameterDescription', {
- defaultMessage: 'Should the runtime automatically move through the pages of the workpad?',
- }),
- getCallRuntimeLabel: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.callRuntimeLabel', {
- defaultMessage: 'Call Runtime',
- }),
- getHeightParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.heightParameterDescription', {
- defaultMessage: 'The height of the Workpad. Defaults to the Workpad height.',
- }),
- getIncludeRuntimeLabel: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.includeRuntimeLabel', {
- defaultMessage: 'Include Runtime',
- }),
- getIntervalParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.intervalParameterDescription', {
- defaultMessage:
- 'The interval upon which the pages will advance in time format, (e.g. {twoSeconds}, {oneMinute})',
- values: {
- twoSeconds: '2s',
- oneMinute: '1m',
- },
- }),
- getPageParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.pageParameterDescription', {
- defaultMessage: 'The page to display. Defaults to the page specified by the Workpad.',
- }),
- getParametersDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersDescription', {
- defaultMessage:
- 'There are a number of inline parameters to configure the Shareable Workpad.',
- }),
- getParametersTitle: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersLabel', {
- defaultMessage: 'Parameters',
- }),
- getPlaceholderLabel: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.placeholderLabel', {
- defaultMessage: 'Placeholder',
- }),
- getRequiredLabel: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.requiredLabel', {
- defaultMessage: 'required',
- }),
- getShareableParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.shareableParameterDescription', {
- defaultMessage: 'The type of shareable. In this case, a {CANVAS} Workpad.',
- values: {
- CANVAS,
- },
- }),
- getSnippetsStepDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.description', {
- defaultMessage:
- 'The Workpad is placed within the {HTML} of the site by using an {HTML} placeholder. Parameters for the runtime are included inline. See the full list of parameters below. You can include more than one workpad on the page.',
- values: {
- HTML,
- },
- }),
- getToolbarParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.toolbarParameterDescription', {
- defaultMessage: 'Should the toolbar be hidden?',
- }),
- getUrlParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.urlParameterDescription', {
- defaultMessage: 'The {URL} of the Shareable Workpad {JSON} file.',
- values: {
- URL,
- JSON,
- },
- }),
- getWidthParameterDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.widthParameterDescription', {
- defaultMessage: 'The width of the Workpad. Defaults to the Workpad width.',
- }),
- },
- ShareWebsiteWorkpadStep: {
- getDownloadLabel: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.downloadLabel', {
- defaultMessage: 'Download workpad',
- }),
- getStepDescription: () =>
- i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.description', {
- defaultMessage:
- 'The workpad will be exported as a single {JSON} file for sharing in another site.',
- values: {
- JSON,
- },
- }),
- },
- SidebarContent: {
- getGroupedElementSidebarTitle: () =>
- i18n.translate('xpack.canvas.sidebarContent.groupedElementSidebarTitle', {
- defaultMessage: 'Grouped element',
- description:
- 'The title displayed when a grouped element is selected. "elements" refer to the different visualizations, images, ' +
- 'text, etc that can be added in a Canvas workpad. These elements can be grouped into a larger "grouped element" ' +
- 'that contains multiple individual elements.',
- }),
- getMultiElementSidebarTitle: () =>
- i18n.translate('xpack.canvas.sidebarContent.multiElementSidebarTitle', {
- defaultMessage: 'Multiple elements',
- description:
- 'The title displayed when multiple elements are selected. "elements" refer to the different visualizations, images, ' +
- 'text, etc that can be added in a Canvas workpad.',
- }),
- getSingleElementSidebarTitle: () =>
- i18n.translate('xpack.canvas.sidebarContent.singleElementSidebarTitle', {
- defaultMessage: 'Selected element',
- description:
- 'The title displayed when a single element are selected. "element" refer to the different visualizations, images, ' +
- 'text, etc that can be added in a Canvas workpad.',
- }),
- },
- SidebarHeader: {
- getBringForwardAriaLabel: () =>
- i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', {
- defaultMessage: 'Move element up one layer',
- }),
- getBringToFrontAriaLabel: () =>
- i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', {
- defaultMessage: 'Move element to top layer',
- }),
- getSendBackwardAriaLabel: () =>
- i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', {
- defaultMessage: 'Move element down one layer',
- }),
- getSendToBackAriaLabel: () =>
- i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', {
- defaultMessage: 'Move element to bottom layer',
- }),
- },
- TextStylePicker: {
- getAlignCenterOption: () =>
- i18n.translate('xpack.canvas.textStylePicker.alignCenterOption', {
- defaultMessage: 'Align center',
- }),
- getAlignLeftOption: () =>
- i18n.translate('xpack.canvas.textStylePicker.alignLeftOption', {
- defaultMessage: 'Align left',
- }),
- getAlignRightOption: () =>
- i18n.translate('xpack.canvas.textStylePicker.alignRightOption', {
- defaultMessage: 'Align right',
- }),
- getAlignmentOptionsControlLegend: () =>
- i18n.translate('xpack.canvas.textStylePicker.alignmentOptionsControl', {
- defaultMessage: 'Alignment options',
- }),
- getFontColorLabel: () =>
- i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', {
- defaultMessage: 'Font Color',
- }),
- getStyleBoldOption: () =>
- i18n.translate('xpack.canvas.textStylePicker.styleBoldOption', {
- defaultMessage: 'Bold',
- }),
- getStyleItalicOption: () =>
- i18n.translate('xpack.canvas.textStylePicker.styleItalicOption', {
- defaultMessage: 'Italic',
- }),
- getStyleUnderlineOption: () =>
- i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', {
- defaultMessage: 'Underline',
- }),
- getStyleOptionsControlLegend: () =>
- i18n.translate('xpack.canvas.textStylePicker.styleOptionsControl', {
- defaultMessage: 'Style options',
- }),
- },
- TimePicker: {
- getApplyButtonLabel: () =>
- i18n.translate('xpack.canvas.timePicker.applyButtonLabel', {
- defaultMessage: 'Apply',
- }),
- },
- Toolbar: {
- getEditorButtonLabel: () =>
- i18n.translate('xpack.canvas.toolbar.editorButtonLabel', {
- defaultMessage: 'Expression editor',
- }),
- getNextPageAriaLabel: () =>
- i18n.translate('xpack.canvas.toolbar.nextPageAriaLabel', {
- defaultMessage: 'Next Page',
- }),
- getPageButtonLabel: (pageNum: number, totalPages: number) =>
- i18n.translate('xpack.canvas.toolbar.pageButtonLabel', {
- defaultMessage: 'Page {pageNum}{rest}',
- values: {
- pageNum,
- rest: totalPages > 1 ? ` of ${totalPages}` : '',
- },
- }),
- getPreviousPageAriaLabel: () =>
- i18n.translate('xpack.canvas.toolbar.previousPageAriaLabel', {
- defaultMessage: 'Previous Page',
- }),
- getWorkpadManagerCloseButtonLabel: () =>
- i18n.translate('xpack.canvas.toolbar.workpadManagerCloseButtonLabel', {
- defaultMessage: 'Close',
- }),
- getErrorMessage: (message: string) =>
- i18n.translate('xpack.canvas.toolbar.errorMessage', {
- defaultMessage: 'TOOLBAR ERROR: {message}',
- values: {
- message,
- },
- }),
- },
- ToolbarTray: {
- getCloseTrayAriaLabel: () =>
- i18n.translate('xpack.canvas.toolbarTray.closeTrayAriaLabel', {
- defaultMessage: 'Close tray',
- }),
- },
- VarConfig: {
- getAddButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfig.addButtonLabel', {
- defaultMessage: 'Add a variable',
- }),
- getAddTooltipLabel: () =>
- i18n.translate('xpack.canvas.varConfig.addTooltipLabel', {
- defaultMessage: 'Add a variable',
- }),
- getCopyActionButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfig.copyActionButtonLabel', {
- defaultMessage: 'Copy snippet',
- }),
- getCopyActionTooltipLabel: () =>
- i18n.translate('xpack.canvas.varConfig.copyActionTooltipLabel', {
- defaultMessage: 'Copy variable syntax to clipboard',
- }),
- getCopyNotificationDescription: () =>
- i18n.translate('xpack.canvas.varConfig.copyNotificationDescription', {
- defaultMessage: 'Variable syntax copied to clipboard',
- }),
- getDeleteActionButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfig.deleteActionButtonLabel', {
- defaultMessage: 'Delete variable',
- }),
- getDeleteNotificationDescription: () =>
- i18n.translate('xpack.canvas.varConfig.deleteNotificationDescription', {
- defaultMessage: 'Variable successfully deleted',
- }),
- getEditActionButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfig.editActionButtonLabel', {
- defaultMessage: 'Edit variable',
- }),
- getEmptyDescription: () =>
- i18n.translate('xpack.canvas.varConfig.emptyDescription', {
- defaultMessage:
- 'This workpad has no variables currently. You may add variables to store and edit common values. These variables can then be used in elements or within the expression editor.',
- }),
- getTableNameLabel: () =>
- i18n.translate('xpack.canvas.varConfig.tableNameLabel', {
- defaultMessage: 'Name',
- }),
- getTableTypeLabel: () =>
- i18n.translate('xpack.canvas.varConfig.tableTypeLabel', {
- defaultMessage: 'Type',
- }),
- getTableValueLabel: () =>
- i18n.translate('xpack.canvas.varConfig.tableValueLabel', {
- defaultMessage: 'Value',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.varConfig.titleLabel', {
- defaultMessage: 'Variables',
- }),
- getTitleTooltip: () =>
- i18n.translate('xpack.canvas.varConfig.titleTooltip', {
- defaultMessage: 'Add variables to store and edit common values',
- }),
- },
- VarConfigDeleteVar: {
- getCancelButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfigDeleteVar.cancelButtonLabel', {
- defaultMessage: 'Cancel',
- }),
- getDeleteButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfigDeleteVar.deleteButtonLabel', {
- defaultMessage: 'Delete variable',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.varConfigDeleteVar.titleLabel', {
- defaultMessage: 'Delete variable?',
- }),
- getWarningDescription: () =>
- i18n.translate('xpack.canvas.varConfigDeleteVar.warningDescription', {
- defaultMessage:
- 'Deleting this variable may adversely affect the workpad. Are you sure you wish to continue?',
- }),
- },
- VarConfigEditVar: {
- getAddTitle: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.addTitleLabel', {
- defaultMessage: 'Add variable',
- }),
- getCancelButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.cancelButtonLabel', {
- defaultMessage: 'Cancel',
- }),
- getDuplicateNameError: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.duplicateNameError', {
- defaultMessage: 'Variable name already in use',
- }),
- getEditTitle: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.editTitleLabel', {
- defaultMessage: 'Edit variable',
- }),
- getEditWarning: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.editWarning', {
- defaultMessage: 'Editing a variable in use may adversely affect your workpad',
- }),
- getNameFieldLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.nameFieldLabel', {
- defaultMessage: 'Name',
- }),
- getSaveButtonLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.saveButtonLabel', {
- defaultMessage: 'Save changes',
- }),
- getTypeBooleanLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.typeBooleanLabel', {
- defaultMessage: 'Boolean',
- }),
- getTypeFieldLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.typeFieldLabel', {
- defaultMessage: 'Type',
- }),
- getTypeNumberLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.typeNumberLabel', {
- defaultMessage: 'Number',
- }),
- getTypeStringLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.typeStringLabel', {
- defaultMessage: 'String',
- }),
- getValueFieldLabel: () =>
- i18n.translate('xpack.canvas.varConfigEditVar.valueFieldLabel', {
- defaultMessage: 'Value',
- }),
- },
- VarConfigVarValueField: {
- getBooleanOptionsLegend: () =>
- i18n.translate('xpack.canvas.varConfigVarValueField.booleanOptionsLegend', {
- defaultMessage: 'Boolean value',
- }),
- getFalseOption: () =>
- i18n.translate('xpack.canvas.varConfigVarValueField.falseOption', {
- defaultMessage: 'False',
- }),
- getTrueOption: () =>
- i18n.translate('xpack.canvas.varConfigVarValueField.trueOption', {
- defaultMessage: 'True',
- }),
- },
- WorkpadConfig: {
- getApplyStylesheetButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadConfig.applyStylesheetButtonLabel', {
- defaultMessage: `Apply stylesheet`,
- 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`,
- }),
- getFlipDimensionTooltip: () =>
- i18n.translate('xpack.canvas.workpadConfig.swapDimensionsTooltip', {
- defaultMessage: 'Swap the width and height',
- }),
- getGlobalCSSLabel: () =>
- i18n.translate('xpack.canvas.workpadConfig.globalCSSLabel', {
- defaultMessage: `Global CSS overrides`,
- }),
- getGlobalCSSTooltip: () =>
- i18n.translate('xpack.canvas.workpadConfig.globalCSSTooltip', {
- defaultMessage: `Apply styles to all pages in this workpad`,
- }),
- getNameLabel: () =>
- i18n.translate('xpack.canvas.workpadConfig.nameLabel', {
- defaultMessage: 'Name',
- }),
- getPageHeightLabel: () =>
- i18n.translate('xpack.canvas.workpadConfig.heightLabel', {
- defaultMessage: 'Height',
- }),
- getPageSizeBadgeAriaLabel: (sizeName: string) =>
- i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeAriaLabel', {
- defaultMessage: `Preset page size: {sizeName}`,
- values: {
- sizeName,
- },
- }),
- getPageSizeBadgeOnClickAriaLabel: (sizeName: string) =>
- i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeOnClickAriaLabel', {
- defaultMessage: `Set page size to {sizeName}`,
- values: {
- sizeName,
- },
- }),
- getPageWidthLabel: () =>
- i18n.translate('xpack.canvas.workpadConfig.widthLabel', {
- defaultMessage: 'Width',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.workpadConfig.title', {
- defaultMessage: 'Workpad settings',
- }),
- getUSLetterButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', {
- defaultMessage: 'US Letter',
- description: 'This is referring to the dimensions of U.S. standard letter paper.',
- }),
- },
- WorkpadCreate: {
- getWorkpadCreateButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', {
- defaultMessage: 'Create workpad',
- }),
- },
- WorkpadHeader: {
- getAddElementButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', {
- defaultMessage: 'Add element',
- }),
- getFullScreenButtonAriaLabel: () =>
- i18n.translate('xpack.canvas.workpadHeader.fullscreenButtonAriaLabel', {
- defaultMessage: 'View fullscreen',
- }),
- getFullScreenTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeader.fullscreenTooltip', {
- defaultMessage: 'Enter fullscreen mode',
- }),
- getHideEditControlTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeader.hideEditControlTooltip', {
- defaultMessage: 'Hide editing controls',
- }),
- getNoWritePermissionTooltipText: () =>
- i18n.translate('xpack.canvas.workpadHeader.noWritePermissionTooltip', {
- defaultMessage: "You don't have permission to edit this workpad",
- }),
- getShowEditControlTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeader.showEditControlTooltip', {
- defaultMessage: 'Show editing controls',
- }),
- },
- WorkpadHeaderAutoRefreshControls: {
- getDisableTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.disableTooltip', {
- defaultMessage: 'Disable auto-refresh',
- }),
- getIntervalFormLabelText: () =>
- i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel', {
- defaultMessage: 'Change auto-refresh interval',
- }),
- getRefreshListDurationManualText: () =>
- i18n.translate(
- 'xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText',
- {
- defaultMessage: 'Manually',
- }
- ),
- getRefreshListTitle: () =>
- i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle', {
- defaultMessage: 'Refresh elements',
- }),
- },
- WorkpadHeaderCustomInterval: {
- getButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel', {
- defaultMessage: 'Set',
- }),
- getFormDescription: () =>
- i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formDescription', {
- defaultMessage:
- 'Use shorthand notation, like {secondsExample}, {minutesExample}, or {hoursExample}',
- values: {
- secondsExample: '30s',
- minutesExample: '10m',
- hoursExample: '1h',
- },
- }),
- getFormLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formLabel', {
- defaultMessage: 'Set a custom interval',
- }),
- },
- WorkpadHeaderEditMenu: {
- getAlignmentMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel', {
- defaultMessage: 'Alignment',
- description:
- 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' +
- 'alignment options of the selected elements',
- }),
- getBottomAlignMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel', {
- defaultMessage: 'Bottom',
- }),
- getCenterAlignMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel', {
- defaultMessage: 'Center',
- description: 'This refers to alignment centered horizontally.',
- }),
- getCreateElementModalTitle: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.createElementModalTitle', {
- defaultMessage: 'Create new element',
- }),
- getDistributionMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel', {
- defaultMessage: 'Distribution',
- description:
- 'This refers to the options to evenly spacing the selected elements horizontall or vertically.',
- }),
- getEditMenuButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuButtonLabel', {
- defaultMessage: 'Edit',
- }),
- getEditMenuLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuLabel', {
- defaultMessage: 'Edit options',
- }),
- getGroupMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel', {
- defaultMessage: 'Group',
- description: 'This refers to grouping multiple selected elements.',
- }),
- getHorizontalDistributionMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel', {
- defaultMessage: 'Horizontal',
- }),
- getLeftAlignMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel', {
- defaultMessage: 'Left',
- }),
- getMiddleAlignMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel', {
- defaultMessage: 'Middle',
- description: 'This refers to alignment centered vertically.',
- }),
- getOrderMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel', {
- defaultMessage: 'Order',
- description: 'Refers to the order of the elements displayed on the page from front to back',
- }),
- getRedoMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.redoMenuItemLabel', {
- defaultMessage: 'Redo',
- }),
- getRightAlignMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel', {
- defaultMessage: 'Right',
- }),
- getSaveElementMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel', {
- defaultMessage: 'Save as new element',
- }),
- getTopAlignMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel', {
- defaultMessage: 'Top',
- }),
- getUndoMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.undoMenuItemLabel', {
- defaultMessage: 'Undo',
- }),
- getUngroupMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel', {
- defaultMessage: 'Ungroup',
- description: 'This refers to ungrouping a grouped element',
- }),
- getVerticalDistributionMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel', {
- defaultMessage: 'Vertical',
- }),
- },
- 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 Kibana',
- }),
- 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', {
- defaultMessage: 'Change cycling interval',
- }),
- getCycleToggleSwitch: () =>
- i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch', {
- defaultMessage: 'Cycle slides automatically',
- }),
- getTitle: () =>
- i18n.translate('xpack.canvas.workpadHeaderKioskControl.controlTitle', {
- defaultMessage: 'Cycle fullscreen pages',
- }),
- getAutoplayListDurationManualText: () =>
- i18n.translate('xpack.canvas.workpadHeaderKioskControl.autoplayListDurationManual', {
- defaultMessage: 'Manually',
- }),
- getDisableTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeaderKioskControl.disableTooltip', {
- defaultMessage: 'Disable auto-play',
- }),
- },
- WorkpadHeaderRefreshControlSettings: {
- getRefreshAriaLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel', {
- defaultMessage: 'Refresh Elements',
- }),
- getRefreshTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip', {
- defaultMessage: 'Refresh data',
- }),
- },
- WorkpadHeaderShareMenu: {
- getCopyPDFMessage: () =>
- i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyPDFMessage', {
- defaultMessage: 'The {PDF} generation {URL} was copied to your clipboard.',
- values: {
- PDF,
- URL,
- },
- }),
- getCopyShareConfigMessage: () =>
- i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', {
- defaultMessage: 'Copied share markup to clipboard',
- }),
- getShareableZipErrorTitle: (workpadName: string) =>
- 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: {
- ZIP,
- workpadName,
- },
- }),
- getShareDownloadJSONTitle: () =>
- i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle', {
- defaultMessage: 'Download as {JSON}',
- values: {
- JSON,
- },
- }),
- getShareDownloadPDFTitle: () =>
- 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.workpadHeaderShareMenu.shareWebsiteTitle', {
- defaultMessage: 'Share on a website',
- }),
- getShareWorkpadMessage: () =>
- i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage', {
- defaultMessage: 'Share this workpad',
- }),
- getUnknownExportErrorMessage: (type: string) =>
- i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
- defaultMessage: 'Unknown export type: {type}',
- values: {
- type,
- },
- }),
- },
- WorkpadHeaderViewMenu: {
- getAutoplayOffMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOffMenuItemLabel', {
- defaultMessage: 'Turn autoplay off',
- }),
- getAutoplayOnMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOnMenuItemLabel', {
- defaultMessage: 'Turn autoplay on',
- }),
- getAutoplaySettingsMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel', {
- defaultMessage: 'Autoplay settings',
- }),
- 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',
- }),
- getRefreshSettingsMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshSettingsMenuItemLabel', {
- defaultMessage: 'Auto refresh settings',
- }),
- 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.workpadHeaderViewMenu.zoomControlsAriaLabel', {
- defaultMessage: 'Zoom controls',
- }),
- getZoomControlsTooltip: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip', {
- defaultMessage: 'Zoom controls',
- }),
- getZoomFitToWindowText: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText', {
- defaultMessage: 'Fit to window',
- }),
- getZoomInText: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomInText', {
- defaultMessage: 'Zoom in',
- }),
- getZoomMenuItemLabel: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel', {
- defaultMessage: 'Zoom',
- }),
- getZoomOutText: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomOutText', {
- defaultMessage: 'Zoom out',
- }),
- getZoomPanelTitle: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle', {
- defaultMessage: 'Zoom',
- }),
- getZoomPercentage: (scale: number) =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomResetText', {
- defaultMessage: '{scalePercentage}%',
- values: {
- scalePercentage: scale * 100,
- },
- }),
- getZoomResetText: () =>
- i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue', {
- defaultMessage: 'Reset',
- }),
- },
- WorkpadLoader: {
- getClonedWorkpadName: (workpadName: string) =>
- i18n.translate('xpack.canvas.workpadLoader.clonedWorkpadName', {
- defaultMessage: 'Copy of {workpadName}',
- values: {
- workpadName,
- },
- description:
- 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' +
- 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"',
- }),
- getCloneToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.cloneTooltip', {
- defaultMessage: 'Clone workpad',
- }),
- getCreateWorkpadLoadingDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.createWorkpadLoadingDescription', {
- defaultMessage: 'Creating workpad...',
- description:
- 'This message appears while the user is waiting for a new workpad to be created',
- }),
- getDeleteButtonAriaLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteButtonAriaLabel', {
- defaultMessage: 'Delete {numberOfWorkpads} workpads',
- values: {
- numberOfWorkpads,
- },
- }),
- getDeleteButtonLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteButtonLabel', {
- defaultMessage: 'Delete ({numberOfWorkpads})',
- values: {
- numberOfWorkpads,
- },
- }),
- getDeleteModalConfirmButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel', {
- defaultMessage: 'Delete',
- }),
- getDeleteModalDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.deleteModalDescription', {
- defaultMessage: `You can't recover deleted workpads.`,
- }),
- getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle', {
- defaultMessage: 'Delete {numberOfWorkpads} workpads?',
- values: {
- numberOfWorkpads,
- },
- }),
- getDeleteSingleWorkpadModalTitle: (workpadName: string) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle', {
- defaultMessage: `Delete workpad '{workpadName}'?`,
- values: {
- workpadName,
- },
- }),
- getEmptyPromptGettingStartedDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription', {
- defaultMessage:
- 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.',
- values: {
- JSON,
- },
- }),
- getEmptyPromptNewUserDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.emptyPromptNewUserDescription', {
- defaultMessage: 'New to {CANVAS}?',
- values: {
- CANVAS,
- },
- }),
- getEmptyPromptTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.emptyPromptTitle', {
- defaultMessage: 'Add your first workpad',
- }),
- getExportButtonAriaLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.exportButtonAriaLabel', {
- defaultMessage: 'Export {numberOfWorkpads} workpads',
- values: {
- numberOfWorkpads,
- },
- }),
- getExportButtonLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.exportButtonLabel', {
- defaultMessage: 'Export ({numberOfWorkpads})',
- values: {
- numberOfWorkpads,
- },
- }),
- getExportToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.exportTooltip', {
- defaultMessage: 'Export workpad',
- }),
- getFetchLoadingDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.fetchLoadingDescription', {
- defaultMessage: 'Fetching workpads...',
- description:
- 'This message appears while the user is waiting for their list of workpads to load',
- }),
- getFilePickerPlaceholder: () =>
- i18n.translate('xpack.canvas.workpadLoader.filePickerPlaceholder', {
- defaultMessage: 'Import workpad {JSON} file',
- values: {
- JSON,
- },
- }),
- getLoadWorkpadArialLabel: (workpadName: string) =>
- i18n.translate('xpack.canvas.workpadLoader.loadWorkpadArialLabel', {
- defaultMessage: `Load workpad '{workpadName}'`,
- values: {
- workpadName,
- },
- }),
- getNoPermissionToCloneToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToCloneToolTip', {
- defaultMessage: `You don't have permission to clone workpads`,
- }),
- getNoPermissionToCreateToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToCreateToolTip', {
- defaultMessage: `You don't have permission to create workpads`,
- }),
- getNoPermissionToDeleteToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToDeleteToolTip', {
- defaultMessage: `You don't have permission to delete workpads`,
- }),
- getNoPermissionToUploadToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToUploadToolTip', {
- defaultMessage: `You don't have permission to upload workpads`,
- }),
- getSampleDataLinkLabel: () =>
- i18n.translate('xpack.canvas.workpadLoader.sampleDataLinkLabel', {
- defaultMessage: 'Add your first workpad',
- }),
- getTableCreatedColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.createdColumnTitle', {
- defaultMessage: 'Created',
- description: 'This column in the table contains the date/time the workpad was created.',
- }),
- getTableNameColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.nameColumnTitle', {
- defaultMessage: 'Workpad name',
- }),
- getTableUpdatedColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.updatedColumnTitle', {
- defaultMessage: 'Updated',
- description:
- 'This column in the table contains the date/time the workpad was last updated.',
- }),
- getTableActionsColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', {
- defaultMessage: 'Actions',
- description:
- 'This column in the table contains the actions that can be taken on a workpad.',
- }),
- },
- WorkpadManager: {
- getModalTitle: () =>
- i18n.translate('xpack.canvas.workpadManager.modalTitle', {
- defaultMessage: '{CANVAS} workpads',
- values: {
- CANVAS,
- },
- }),
- getMyWorkpadsTabLabel: () =>
- i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', {
- defaultMessage: 'My workpads',
- }),
- getWorkpadTemplatesTabLabel: () =>
- i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', {
- defaultMessage: 'Templates',
- description: 'The label for the tab that displays a list of designed workpad templates.',
- }),
- },
- WorkpadSearch: {
- getWorkpadSearchPlaceholder: () =>
- i18n.translate('xpack.canvas.workpadSearch.searchPlaceholder', {
- defaultMessage: 'Find workpad',
- }),
- },
- WorkpadTemplates: {
- getCloneTemplateLinkAriaLabel: (templateName: string) =>
- i18n.translate('xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel', {
- defaultMessage: `Clone workpad template '{templateName}'`,
- values: {
- templateName,
- },
- }),
- getTableDescriptionColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', {
- defaultMessage: 'Description',
- }),
- getTableNameColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', {
- defaultMessage: 'Template name',
- }),
- getTableTagsColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', {
- defaultMessage: 'Tags',
- description:
- 'This column contains relevant tags that indicate what type of template ' +
- 'is displayed. For example: "report", "presentation", etc.',
- }),
- getTemplateSearchPlaceholder: () =>
- i18n.translate('xpack.canvas.workpadTemplate.searchPlaceholder', {
- defaultMessage: 'Find template',
- }),
- getCreatingTemplateLabel: (templateName: string) =>
- i18n.translate('xpack.canvas.workpadTemplate.creatingTemplateLabel', {
- defaultMessage: `Creating from template '{templateName}'`,
- values: {
- templateName,
- },
- }),
- },
-};
diff --git a/x-pack/plugins/canvas/i18n/errors.ts b/x-pack/plugins/canvas/i18n/errors.ts
index 0928045119234..a55762dce2d20 100644
--- a/x-pack/plugins/canvas/i18n/errors.ts
+++ b/x-pack/plugins/canvas/i18n/errors.ts
@@ -6,7 +6,6 @@
*/
import { i18n } from '@kbn/i18n';
-import { CANVAS, JSON } from './constants';
export const ErrorStrings = {
actionsElements: {
@@ -93,54 +92,10 @@ export const ErrorStrings = {
},
}),
},
- WorkpadFileUpload: {
- getAcceptJSONOnlyErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage', {
- defaultMessage: 'Only {JSON} files are accepted',
- values: {
- JSON,
- },
- }),
- getFileUploadFailureWithFileNameErrorMessage: (fileName: string) =>
- i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', {
- defaultMessage: `Couldn't upload '{fileName}'`,
- values: {
- fileName,
- },
- }),
- getFileUploadFailureWithoutFileNameErrorMessage: () =>
- i18n.translate(
- 'xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage',
- {
- defaultMessage: `Couldn't upload file`,
- }
- ),
- getMissingPropertiesErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', {
- defaultMessage:
- 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.',
- values: {
- CANVAS,
- JSON,
- },
- }),
- },
- WorkpadLoader: {
- getCloneFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', {
- defaultMessage: `Couldn't clone workpad`,
- }),
- getDeleteFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.deleteFailureErrorMessage', {
- defaultMessage: `Couldn't delete all workpads`,
- }),
- getFindFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.findFailureErrorMessage', {
- defaultMessage: `Couldn't find workpad`,
- }),
- getUploadFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', {
- defaultMessage: `Couldn't upload workpad`,
+ WorkpadDropzone: {
+ getTooManyFilesErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.workpadDropzone.tooManyFilesErrorMessage', {
+ defaultMessage: 'One one file can be uploaded at a time',
}),
},
workpadRoutes: {
diff --git a/x-pack/plugins/canvas/i18n/index.ts b/x-pack/plugins/canvas/i18n/index.ts
index 14c9e5d221b79..d35b915ea7fb6 100644
--- a/x-pack/plugins/canvas/i18n/index.ts
+++ b/x-pack/plugins/canvas/i18n/index.ts
@@ -6,7 +6,6 @@
*/
export * from './capabilities';
-export * from './components';
export * from './constants';
export * from './errors';
export * from './expression_types';
diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx
index 194d2d8b3ddf5..d9df1e4661fbf 100644
--- a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx
+++ b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx
@@ -8,15 +8,20 @@
import React, { MouseEventHandler, FC } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
// @ts-expect-error untyped local
import { Popover, PopoverChildrenProps } from '../popover';
import { ArgAdd } from '../arg_add';
// @ts-expect-error untyped local
import { Arg } from '../../expression_types/arg';
-import { ComponentStrings } from '../../../i18n';
-
-const { ArgAddPopover: strings } = ComponentStrings;
+const strings = {
+ getAddAriaLabel: () =>
+ i18n.translate('xpack.canvas.argAddPopover.addAriaLabel', {
+ defaultMessage: 'Add argument',
+ }),
+};
interface ArgOptions {
arg: Arg;
diff --git a/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js b/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js
index c40e74186e87e..14f47553002ac 100644
--- a/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js
+++ b/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js
@@ -9,12 +9,25 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose, withProps, withPropsOnChange } from 'recompose';
import { EuiTextArea, EuiButton, EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { fromExpression, toExpression } from '@kbn/interpreter/common';
-import { createStatefulPropHoc } from '../../components/enhance/stateful_prop';
-import { ComponentStrings } from '../../../i18n';
+import { createStatefulPropHoc } from '../../components/enhance/stateful_prop';
-const { ArgFormAdvancedFailure: strings } = ComponentStrings;
+const strings = {
+ getApplyButtonLabel: () =>
+ i18n.translate('xpack.canvas.argFormAdvancedFailure.applyButtonLabel', {
+ defaultMessage: 'Apply',
+ }),
+ getResetButtonLabel: () =>
+ i18n.translate('xpack.canvas.argFormAdvancedFailure.resetButtonLabel', {
+ defaultMessage: 'Reset',
+ }),
+ getRowErrorMessage: () =>
+ i18n.translate('xpack.canvas.argFormAdvancedFailure.rowErrorMessage', {
+ defaultMessage: 'Invalid Expression',
+ }),
+};
export const AdvancedFailureComponent = (props) => {
const {
diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx b/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx
index 2ae772cdc197a..84b87373c1c5a 100644
--- a/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx
+++ b/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx
@@ -8,12 +8,20 @@
import React, { ReactNode, MouseEventHandler } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
-import { TooltipIcon, IconType } from '../tooltip_icon';
-
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
-const { ArgFormArgSimpleForm: strings } = ComponentStrings;
+import { TooltipIcon, IconType } from '../tooltip_icon';
+const strings = {
+ getRemoveAriaLabel: () =>
+ i18n.translate('xpack.canvas.argFormArgSimpleForm.removeAriaLabel', {
+ defaultMessage: 'Remove',
+ }),
+ getRequiredTooltip: () =>
+ i18n.translate('xpack.canvas.argFormArgSimpleForm.requiredTooltip', {
+ defaultMessage: 'This argument is required, you should specify a value.',
+ }),
+};
interface Props {
children?: ReactNode;
required?: boolean;
diff --git a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js
index ff390a770f80e..f933230f39928 100644
--- a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js
+++ b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js
@@ -7,11 +7,17 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
+
import { Loading } from '../loading';
import { ArgLabel } from './arg_label';
-const { ArgFormPendingArgValue: strings } = ComponentStrings;
+const strings = {
+ getLoadingMessage: () =>
+ i18n.translate('xpack.canvas.argFormPendingArgValue.loadingMessage', {
+ defaultMessage: 'Loading',
+ }),
+};
export class PendingArgValue extends React.PureComponent {
static propTypes = {
diff --git a/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx b/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx
index cc4e92679a870..57173fa413e8f 100644
--- a/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx
+++ b/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx
@@ -6,11 +6,17 @@
*/
import React from 'react';
-import { TooltipIcon, IconType } from '../tooltip_icon';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../i18n';
+import { TooltipIcon, IconType } from '../tooltip_icon';
-const { ArgFormSimpleFailure: strings } = ComponentStrings;
+const strings = {
+ getFailureTooltip: () =>
+ i18n.translate('xpack.canvas.argFormSimpleFailure.failureTooltip', {
+ defaultMessage:
+ 'The interface for this argument could not parse the value, so a fallback input is being used',
+ }),
+};
// This is what is being generated by render() from the Arg class. It is called in FunctionForm
export const SimpleFailure = () => (
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot
index 34b6b333f3ef5..d567d3cf85f13 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot
@@ -116,20 +116,13 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = `
size="xxl"
/>
-
-
- Import your assets to get started
-
-
-
+ Import your assets to get started
+
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
index 8f9d90ccbe1d8..024137f640636 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
@@ -17,6 +17,7 @@ import {
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { useNotifyService } from '../../services';
@@ -25,9 +26,40 @@ import { Clipboard } from '../clipboard';
import { Download } from '../download';
import { AssetType } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-
-const { Asset: strings } = ComponentStrings;
+const strings = {
+ getCopyAssetTooltip: () =>
+ i18n.translate('xpack.canvas.asset.copyAssetTooltip', {
+ defaultMessage: 'Copy id to clipboard',
+ }),
+ getCreateImageTooltip: () =>
+ i18n.translate('xpack.canvas.asset.createImageTooltip', {
+ defaultMessage: 'Create image element',
+ }),
+ getDeleteAssetTooltip: () =>
+ i18n.translate('xpack.canvas.asset.deleteAssetTooltip', {
+ defaultMessage: 'Delete',
+ }),
+ getDownloadAssetTooltip: () =>
+ i18n.translate('xpack.canvas.asset.downloadAssetTooltip', {
+ defaultMessage: 'Download',
+ }),
+ getThumbnailAltText: () =>
+ i18n.translate('xpack.canvas.asset.thumbnailAltText', {
+ defaultMessage: 'Asset thumbnail',
+ }),
+ getConfirmModalButtonLabel: () =>
+ i18n.translate('xpack.canvas.asset.confirmModalButtonLabel', {
+ defaultMessage: 'Remove',
+ }),
+ getConfirmModalMessageText: () =>
+ i18n.translate('xpack.canvas.asset.confirmModalDetail', {
+ defaultMessage: 'Are you sure you want to remove this asset?',
+ }),
+ getConfirmModalTitle: () =>
+ i18n.translate('xpack.canvas.asset.confirmModalTitle', {
+ defaultMessage: 'Remove Asset',
+ }),
+};
export interface Props {
/** The asset to be rendered */
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
index 7795aa9671b83..7b004d5ab5099 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
@@ -24,14 +24,47 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { ASSET_MAX_SIZE } from '../../../common/lib/constants';
import { Loading } from '../loading';
import { Asset } from './asset';
import { AssetType } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-const { AssetManager: strings } = ComponentStrings;
+const strings = {
+ getDescription: () =>
+ i18n.translate('xpack.canvas.assetModal.modalDescription', {
+ defaultMessage:
+ '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.',
+ }),
+ getEmptyAssetsDescription: () =>
+ i18n.translate('xpack.canvas.assetModal.emptyAssetsDescription', {
+ defaultMessage: 'Import your assets to get started',
+ }),
+ getFilePickerPromptText: () =>
+ i18n.translate('xpack.canvas.assetModal.filePickerPromptText', {
+ defaultMessage: 'Select or drag and drop images',
+ }),
+ getLoadingText: () =>
+ i18n.translate('xpack.canvas.assetModal.loadingText', {
+ defaultMessage: 'Uploading images',
+ }),
+ getModalCloseButtonLabel: () =>
+ i18n.translate('xpack.canvas.assetModal.modalCloseButtonLabel', {
+ defaultMessage: 'Close',
+ }),
+ getModalTitle: () =>
+ i18n.translate('xpack.canvas.assetModal.modalTitle', {
+ defaultMessage: 'Manage workpad assets',
+ }),
+ getSpaceUsedText: (percentageUsed: number) =>
+ i18n.translate('xpack.canvas.assetModal.spacedUsedText', {
+ defaultMessage: '{percentageUsed}% space used',
+ values: {
+ percentageUsed,
+ },
+ }),
+};
export interface Props {
/** The assets to display within the modal */
diff --git a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx
index c2e2d8a053247..4bf13577aff53 100644
--- a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx
@@ -8,12 +8,16 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGrid, EuiFlexItem, EuiLink, EuiImage, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { CanvasAsset } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-
-const { AssetPicker: strings } = ComponentStrings;
+const strings = {
+ getAssetAltText: () =>
+ i18n.translate('xpack.canvas.assetpicker.assetAltText', {
+ defaultMessage: 'Asset thumbnail',
+ }),
+};
interface Props {
assets: CanvasAsset[];
diff --git a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx
index 38e62f46c945a..8f55c31933291 100644
--- a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx
+++ b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx
@@ -7,9 +7,14 @@
import React, { FC } from 'react';
import { EuiPanel, EuiLoadingChart, EuiSpacer, EuiText } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n/components';
+import { i18n } from '@kbn/i18n';
-const { CanvasLoading: strings } = ComponentStrings;
+const strings = {
+ getLoadingLabel: () =>
+ i18n.translate('xpack.canvas.canvasLoading.loadingMessage', {
+ defaultMessage: 'Loading',
+ }),
+};
export const CanvasLoading: FC<{ msg?: string }> = ({
msg = `${strings.getLoadingLabel()}...`,
diff --git a/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx b/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx
index ae5cfac85bdc9..50c679c2a1e51 100644
--- a/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx
+++ b/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx
@@ -9,11 +9,24 @@ import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import tinycolor from 'tinycolor2';
-import { ColorDot } from '../color_dot/color_dot';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../i18n/components';
+import { ColorDot } from '../color_dot/color_dot';
-const { ColorManager: strings } = ComponentStrings;
+const strings = {
+ getAddAriaLabel: () =>
+ i18n.translate('xpack.canvas.colorManager.addAriaLabel', {
+ defaultMessage: 'Add Color',
+ }),
+ getCodePlaceholder: () =>
+ i18n.translate('xpack.canvas.colorManager.codePlaceholder', {
+ defaultMessage: 'Color code',
+ }),
+ getRemoveAriaLabel: () =>
+ i18n.translate('xpack.canvas.colorManager.removeAriaLabel', {
+ defaultMessage: 'Remove Color',
+ }),
+};
export interface Props {
/**
diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot
index 18f86aca24302..dc66eef809050 100644
--- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot
@@ -80,7 +80,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`]
className="euiFormControlLayout__childrenWrapper"
>
40 characters remaining
@@ -119,7 +119,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`]
className="euiFormRow__fieldWrapper"
>
83 characters remaining
@@ -389,7 +389,7 @@ exports[`Storyshots components/Elements/CustomElementModal with image 1`] = `
className="euiFormControlLayout__childrenWrapper"
>
40 characters remaining
@@ -428,7 +428,7 @@ exports[`Storyshots components/Elements/CustomElementModal with image 1`] = `
className="euiFormRow__fieldWrapper"
>
100 characters remaining
@@ -695,7 +695,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = `
className="euiFormControlLayout__childrenWrapper"
>
32 characters remaining
@@ -734,7 +734,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = `
className="euiFormRow__fieldWrapper"
>
100 characters remaining
@@ -996,7 +996,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = `
className="euiFormControlLayout__childrenWrapper"
>
40 characters remaining
@@ -1035,7 +1035,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = `
className="euiFormRow__fieldWrapper"
>
100 characters remaining
diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
index 5d9cccba924a9..86d9cab4eeea1 100644
--- a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
+++ b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
@@ -26,16 +26,57 @@ import {
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
import { encode } from '../../../common/lib/dataurl';
import { ElementCard } from '../element_card';
-import { ComponentStrings } from '../../../i18n/components';
const MAX_NAME_LENGTH = 40;
const MAX_DESCRIPTION_LENGTH = 100;
-const { CustomElementModal: strings } = ComponentStrings;
-
+const strings = {
+ getCancelButtonLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+ getCharactersRemainingDescription: (numberOfRemainingCharacter: number) =>
+ i18n.translate('xpack.canvas.customElementModal.remainingCharactersDescription', {
+ defaultMessage: '{numberOfRemainingCharacter} characters remaining',
+ values: {
+ numberOfRemainingCharacter,
+ },
+ }),
+ getDescriptionInputLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.descriptionInputLabel', {
+ defaultMessage: 'Description',
+ }),
+ getElementPreviewTitle: () =>
+ i18n.translate('xpack.canvas.customElementModal.elementPreviewTitle', {
+ defaultMessage: 'Element preview',
+ }),
+ getImageFilePickerPlaceholder: () =>
+ i18n.translate('xpack.canvas.customElementModal.imageFilePickerPlaceholder', {
+ defaultMessage: 'Select or drag and drop an image',
+ }),
+ getImageInputDescription: () =>
+ i18n.translate('xpack.canvas.customElementModal.imageInputDescription', {
+ defaultMessage:
+ 'Take a screenshot of your element and upload it here. This can also be done after saving.',
+ }),
+ getImageInputLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.imageInputLabel', {
+ defaultMessage: 'Thumbnail image',
+ }),
+ getNameInputLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.nameInputLabel', {
+ defaultMessage: 'Name',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.saveButtonLabel', {
+ defaultMessage: 'Save',
+ }),
+};
interface Props {
/**
* initial value of the name of the custom element
diff --git a/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot b/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot
index 6d170d78dd01d..836047959caee 100644
--- a/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot
@@ -39,9 +39,13 @@ exports[`Storyshots components/datasource/DatasourceComponent datasource with ex
-
- The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.
-
+
+
+ The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.
+
+
diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js
index faddc3a60b990..f09ce4c925820 100644
--- a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js
+++ b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js
@@ -18,13 +18,27 @@ import {
EuiHorizontalRule,
} from '@elastic/eui';
import { isEqual } from 'lodash';
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
+
import { getDefaultIndex } from '../../lib/es_service';
import { DatasourceSelector } from './datasource_selector';
import { DatasourcePreview } from './datasource_preview';
-const { DatasourceDatasourceComponent: strings } = ComponentStrings;
-
+const strings = {
+ getExpressionArgDescription: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourceComponent.expressionArgDescription', {
+ defaultMessage:
+ 'The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.',
+ }),
+ getPreviewButtonLabel: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', {
+ defaultMessage: 'Preview data',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', {
+ defaultMessage: 'Save',
+ }),
+};
export class DatasourceComponent extends PureComponent {
static propTypes = {
args: PropTypes.object.isRequired,
diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js
index a55f73a087467..2eb42c5cb98dc 100644
--- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js
+++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js
@@ -18,12 +18,33 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
import { Datatable } from '../../datatable';
import { Error } from '../../error';
-import { ComponentStrings } from '../../../../i18n';
-const { DatasourceDatasourcePreview: strings } = ComponentStrings;
-const { DatasourceDatasourceComponent: datasourceStrings } = ComponentStrings;
+const strings = {
+ getEmptyFirstLineDescription: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyFirstLineDescription', {
+ defaultMessage: "We couldn't find any documents matching your search criteria.",
+ }),
+ getEmptySecondLineDescription: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptySecondLineDescription', {
+ defaultMessage: 'Check your datasource settings and try again.',
+ }),
+ getEmptyTitle: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyTitle', {
+ defaultMessage: 'No documents found',
+ }),
+ getModalTitle: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.modalTitle', {
+ defaultMessage: 'Datasource preview',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.saveButtonLabel', {
+ defaultMessage: 'Save',
+ }),
+};
export const DatasourcePreview = ({ done, datatable }) => (
@@ -37,7 +58,7 @@ export const DatasourcePreview = ({ done, datatable }) => (
id="xpack.canvas.datasourceDatasourcePreview.modalDescription"
defaultMessage="The following data will be available to the selected element upon clicking {saveLabel} in the sidebar."
values={{
- saveLabel: {datasourceStrings.getSaveButtonLabel()} ,
+ saveLabel: {strings.getSaveButtonLabel()} ,
}}
/>
diff --git a/x-pack/plugins/canvas/public/components/datasource/no_datasource.js b/x-pack/plugins/canvas/public/components/datasource/no_datasource.js
index ef86361a4a3a0..f496d493e9d94 100644
--- a/x-pack/plugins/canvas/public/components/datasource/no_datasource.js
+++ b/x-pack/plugins/canvas/public/components/datasource/no_datasource.js
@@ -8,9 +8,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../i18n';
-const { DatasourceNoDatasource: strings } = ComponentStrings;
+const strings = {
+ getPanelDescription: () =>
+ i18n.translate('xpack.canvas.datasourceNoDatasource.panelDescription', {
+ defaultMessage:
+ "This element does not have an attached data source. This is usually because the element is an image or other static asset. If that's not the case you might want to check your expression to make sure it is not malformed.",
+ }),
+ getPanelTitle: () =>
+ i18n.translate('xpack.canvas.datasourceNoDatasource.panelTitle', {
+ defaultMessage: 'No data source present',
+ }),
+};
export const NoDatasource = () => (
diff --git a/x-pack/plugins/canvas/public/components/element_config/element_config.tsx b/x-pack/plugins/canvas/public/components/element_config/element_config.tsx
index 683c12f13f0f9..bf09ac3c5ab77 100644
--- a/x-pack/plugins/canvas/public/components/element_config/element_config.tsx
+++ b/x-pack/plugins/canvas/public/components/element_config/element_config.tsx
@@ -5,13 +5,42 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion } from '@elastic/eui';
-import PropTypes from 'prop-types';
import React from 'react';
-import { ComponentStrings } from '../../../i18n';
+import PropTypes from 'prop-types';
+import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { State } from '../../../types';
-const { ElementConfig: strings } = ComponentStrings;
+const strings = {
+ getFailedLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.failedLabel', {
+ defaultMessage: 'Failed',
+ description:
+ 'The label for the total number of elements in a workpad that have thrown an error or failed to load',
+ }),
+ getLoadedLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.loadedLabel', {
+ defaultMessage: 'Loaded',
+ description: 'The label for the number of elements in a workpad that have loaded',
+ }),
+ getProgressLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.progressLabel', {
+ defaultMessage: 'Progress',
+ description: 'The label for the percentage of elements that have finished loading',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.elementConfig.title', {
+ defaultMessage: 'Element status',
+ description:
+ '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad',
+ }),
+ getTotalLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.totalLabel', {
+ defaultMessage: 'Total',
+ description: 'The label for the total number of elements in a workpad',
+ }),
+};
interface Props {
elementStats: State['transient']['elementStats'];
diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
index c86b1d6405e24..716f757b7c25e 100644
--- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
+++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
@@ -7,15 +7,24 @@
import React, { FC } from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import {
SavedObjectFinderUi,
SavedObjectMetaData,
} from '../../../../../../src/plugins/saved_objects/public/';
-import { ComponentStrings } from '../../../i18n';
import { useServices } from '../../services';
-const { AddEmbeddableFlyout: strings } = ComponentStrings;
-
+const strings = {
+ getNoItemsText: () =>
+ i18n.translate('xpack.canvas.embedObject.noMatchingObjectsMessage', {
+ defaultMessage: 'No matching objects found.',
+ }),
+ getTitleText: () =>
+ i18n.translate('xpack.canvas.embedObject.titleText', {
+ defaultMessage: 'Add from Kibana',
+ }),
+};
export interface Props {
onClose: () => void;
onSelect: (id: string, embeddableType: string) => void;
diff --git a/x-pack/plugins/canvas/public/components/error/error.tsx b/x-pack/plugins/canvas/public/components/error/error.tsx
index b4cc85ba336e9..cb2c2cd5d58c1 100644
--- a/x-pack/plugins/canvas/public/components/error/error.tsx
+++ b/x-pack/plugins/canvas/public/components/error/error.tsx
@@ -8,18 +8,27 @@
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
-import { ComponentStrings } from '../../../i18n';
+
import { ShowDebugging } from './show_debugging';
+const strings = {
+ getDescription: () =>
+ i18n.translate('xpack.canvas.errorComponent.description', {
+ defaultMessage: 'Expression failed with the message:',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.errorComponent.title', {
+ defaultMessage: 'Whoops! Expression failed',
+ }),
+};
export interface Props {
payload: {
error: Error;
};
}
-const { Error: strings } = ComponentStrings;
-
export const Error: FC
= ({ payload }) => {
const message = get(payload, 'error.message');
diff --git a/x-pack/plugins/canvas/public/components/expression/element_not_selected.js b/x-pack/plugins/canvas/public/components/expression/element_not_selected.js
index c7c8c1b063cf1..5f717af6101c1 100644
--- a/x-pack/plugins/canvas/public/components/expression/element_not_selected.js
+++ b/x-pack/plugins/canvas/public/components/expression/element_not_selected.js
@@ -8,9 +8,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiButton } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
-const { ExpressionElementNotSelected: strings } = ComponentStrings;
+const strings = {
+ getCloseButtonLabel: () =>
+ i18n.translate('xpack.canvas.expressionElementNotSelected.closeButtonLabel', {
+ defaultMessage: 'Close',
+ }),
+ getSelectDescription: () =>
+ i18n.translate('xpack.canvas.expressionElementNotSelected.selectDescription', {
+ defaultMessage: 'Select an element to show expression input',
+ }),
+};
export const ElementNotSelected = ({ done }) => (
diff --git a/x-pack/plugins/canvas/public/components/expression/expression.tsx b/x-pack/plugins/canvas/public/components/expression/expression.tsx
index 74fdefc322cc9..ff3fed32c0ac0 100644
--- a/x-pack/plugins/canvas/public/components/expression/expression.tsx
+++ b/x-pack/plugins/canvas/public/components/expression/expression.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { FC, MutableRefObject } from 'react';
+import React, { FC, MutableRefObject, useRef } from 'react';
import PropTypes from 'prop-types';
import {
EuiPanel,
@@ -17,17 +17,46 @@ import {
EuiLink,
EuiPortal,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
// @ts-expect-error
import { Shortcuts } from 'react-shortcuts';
-import { ComponentStrings } from '../../../i18n';
+
import { ExpressionInput } from '../expression_input';
import { ToolTipShortcut } from '../tool_tip_shortcut';
import { ExpressionFunction } from '../../../types';
import { FormState } from './';
-const { Expression: strings } = ComponentStrings;
-
-const { useRef } = React;
+const strings = {
+ getCancelButtonLabel: () =>
+ i18n.translate('xpack.canvas.expression.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+ getCloseButtonLabel: () =>
+ i18n.translate('xpack.canvas.expression.closeButtonLabel', {
+ defaultMessage: 'Close',
+ }),
+ getLearnLinkText: () =>
+ i18n.translate('xpack.canvas.expression.learnLinkText', {
+ defaultMessage: 'Learn expression syntax',
+ }),
+ getMaximizeButtonLabel: () =>
+ i18n.translate('xpack.canvas.expression.maximizeButtonLabel', {
+ defaultMessage: 'Maximize editor',
+ }),
+ getMinimizeButtonLabel: () =>
+ i18n.translate('xpack.canvas.expression.minimizeButtonLabel', {
+ defaultMessage: 'Minimize Editor',
+ }),
+ getRunButtonLabel: () =>
+ i18n.translate('xpack.canvas.expression.runButtonLabel', {
+ defaultMessage: 'Run',
+ }),
+ getRunTooltip: () =>
+ i18n.translate('xpack.canvas.expression.runTooltip', {
+ defaultMessage: 'Run the expression',
+ }),
+};
const shortcut = (
ref: MutableRefObject
,
diff --git a/x-pack/plugins/canvas/public/components/expression_input/reference.ts b/x-pack/plugins/canvas/public/components/expression_input/reference.ts
index 95d27360aafc9..94a369e6cb8d8 100644
--- a/x-pack/plugins/canvas/public/components/expression_input/reference.ts
+++ b/x-pack/plugins/canvas/public/components/expression_input/reference.ts
@@ -5,13 +5,64 @@
* 2.0.
*/
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ExpressionFunctionParameter,
} from '../../../../../../src/plugins/expressions';
-const { ExpressionInput: strings } = ComponentStrings;
+import { BOLD_MD_TOKEN } from '../../../i18n/constants';
+
+const strings = {
+ getArgReferenceAliasesDetail: (aliases: string) =>
+ i18n.translate('xpack.canvas.expressionInput.argReferenceAliasesDetail', {
+ defaultMessage: '{BOLD_MD_TOKEN}Aliases{BOLD_MD_TOKEN}: {aliases}',
+ values: {
+ BOLD_MD_TOKEN,
+ aliases,
+ },
+ }),
+ getArgReferenceDefaultDetail: (defaultVal: string) =>
+ i18n.translate('xpack.canvas.expressionInput.argReferenceDefaultDetail', {
+ defaultMessage: '{BOLD_MD_TOKEN}Default{BOLD_MD_TOKEN}: {defaultVal}',
+ values: {
+ BOLD_MD_TOKEN,
+ defaultVal,
+ },
+ }),
+ getArgReferenceRequiredDetail: (required: string) =>
+ i18n.translate('xpack.canvas.expressionInput.argReferenceRequiredDetail', {
+ defaultMessage: '{BOLD_MD_TOKEN}Required{BOLD_MD_TOKEN}: {required}',
+ values: {
+ BOLD_MD_TOKEN,
+ required,
+ },
+ }),
+ getArgReferenceTypesDetail: (types: string) =>
+ i18n.translate('xpack.canvas.expressionInput.argReferenceTypesDetail', {
+ defaultMessage: '{BOLD_MD_TOKEN}Types{BOLD_MD_TOKEN}: {types}',
+ values: {
+ BOLD_MD_TOKEN,
+ types,
+ },
+ }),
+ getFunctionReferenceAcceptsDetail: (acceptTypes: string) =>
+ i18n.translate('xpack.canvas.expressionInput.functionReferenceAccepts', {
+ defaultMessage: '{BOLD_MD_TOKEN}Accepts{BOLD_MD_TOKEN}: {acceptTypes}',
+ values: {
+ BOLD_MD_TOKEN,
+ acceptTypes,
+ },
+ }),
+ getFunctionReferenceReturnsDetail: (returnType: string) =>
+ i18n.translate('xpack.canvas.expressionInput.functionReferenceReturns', {
+ defaultMessage: '{BOLD_MD_TOKEN}Returns{BOLD_MD_TOKEN}: {returnType}',
+ values: {
+ BOLD_MD_TOKEN,
+ returnType,
+ },
+ }),
+};
/**
* Given an expression function, this function returns a markdown string
diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx b/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx
index a022f98d14e1a..2ee709edbf91c 100644
--- a/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx
+++ b/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx
@@ -7,16 +7,23 @@
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
-import { ComponentStrings } from '../../../i18n/components';
+import { i18n } from '@kbn/i18n';
+const strings = {
+ getContextErrorMessage: (errorMessage: string) =>
+ i18n.translate('xpack.canvas.functionForm.contextError', {
+ defaultMessage: 'ERROR: {errorMessage}',
+ values: {
+ errorMessage,
+ },
+ }),
+};
interface Props {
context: {
error: string;
};
}
-const { FunctionFormContextError: strings } = ComponentStrings;
-
export const FunctionFormContextError: FunctionComponent = ({ context }) => (
{strings.getContextErrorMessage(context.error)}
diff --git a/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx b/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx
index b3054e280bbe5..cd7e2f27912a1 100644
--- a/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx
+++ b/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx
@@ -7,13 +7,22 @@
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
+
+const strings = {
+ getUnknownArgumentTypeErrorMessage: (expressionType: string) =>
+ i18n.translate('xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError', {
+ defaultMessage: 'Unknown expression type "{expressionType}"',
+ values: {
+ expressionType,
+ },
+ }),
+};
interface Props {
/** the type of the argument */
argType: string;
}
-const { FunctionFormFunctionUnknown: strings } = ComponentStrings;
export const FunctionUnknown: FunctionComponent
= ({ argType }) => (
diff --git a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx
index b10103e1824e5..2877ccf41056d 100644
--- a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx
+++ b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx
@@ -7,11 +7,13 @@
import React, { FC, useState, lazy, Suspense } from 'react';
import { EuiButtonEmpty, EuiPortal, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { ExpressionFunction } from 'src/plugins/expressions';
-import { ComponentStrings } from '../../../i18n';
+
import { KeyboardShortcutsDoc } from '../keyboard_shortcuts_doc';
let FunctionReferenceGenerator: null | React.LazyExoticComponent
= null;
+
if (process.env.NODE_ENV === 'development') {
FunctionReferenceGenerator = lazy(() =>
import('../function_reference_generator').then((module) => ({
@@ -20,7 +22,12 @@ if (process.env.NODE_ENV === 'development') {
);
}
-const { HelpMenu: strings } = ComponentStrings;
+const strings = {
+ getKeyboardShortcutsLinkLabel: () =>
+ i18n.translate('xpack.canvas.helpMenu.keyboardShortcutsLinkLabel', {
+ defaultMessage: 'Keyboard shortcuts',
+ }),
+};
interface Props {
functionRegistry: Record;
diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx
new file mode 100644
index 0000000000000..96a773186da2b
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public';
+import { withSuspense } from '../../../../../../src/plugins/presentation_util/public';
+
+import { WorkpadCreate } from './workpad_create';
+import { LazyWorkpadTemplates } from './workpad_templates';
+import { LazyMyWorkpads } from './my_workpads';
+
+export type HomePageTab = 'workpads' | 'templates';
+
+export interface Props {
+ activeTab?: HomePageTab;
+}
+
+const WorkpadTemplates = withSuspense(LazyWorkpadTemplates);
+const MyWorkpads = withSuspense(LazyMyWorkpads);
+
+export const Home = ({ activeTab = 'workpads' }: Props) => {
+ const [tab, setTab] = useState(activeTab);
+
+ return (
+ ],
+ bottomBorder: true,
+ tabs: [
+ {
+ label: strings.getMyWorkpadsTabLabel(),
+ id: 'myWorkpads',
+ isSelected: tab === 'workpads',
+ onClick: () => setTab('workpads'),
+ },
+ {
+ label: strings.getWorkpadTemplatesTabLabel(),
+ id: 'workpadTemplates',
+ 'data-test-subj': 'workpadTemplates',
+ isSelected: tab === 'templates',
+ onClick: () => setTab('templates'),
+ },
+ ],
+ }}
+ >
+ {tab === 'workpads' ? : }
+
+ );
+};
+
+const strings = {
+ getMyWorkpadsTabLabel: () =>
+ i18n.translate('xpack.canvas.home.myWorkpadsTabLabel', {
+ defaultMessage: 'My workpads',
+ }),
+ getWorkpadTemplatesTabLabel: () =>
+ i18n.translate('xpack.canvas.home.workpadTemplatesTabLabel', {
+ defaultMessage: 'Templates',
+ description: 'The label for the tab that displays a list of designed workpad templates.',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/home.stories.tsx b/x-pack/plugins/canvas/public/components/home/home.stories.tsx
new file mode 100644
index 0000000000000..186b916afa003
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/home.stories.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import {
+ reduxDecorator,
+ getAddonPanelParameters,
+ servicesContextDecorator,
+ getDisableStoryshotsParameter,
+} from '../../../storybook';
+
+import { Home } from './home.component';
+
+export default {
+ title: 'Home/Home Page',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoContent = () => ;
+export const HasContent = () => ;
+
+NoContent.decorators = [servicesContextDecorator()];
+HasContent.decorators = [servicesContextDecorator({ findWorkpads: 5, findTemplates: true })];
diff --git a/x-pack/plugins/canvas/public/components/home/home.tsx b/x-pack/plugins/canvas/public/components/home/home.tsx
new file mode 100644
index 0000000000000..6b356ada8681e
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/home.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+import { getBaseBreadcrumb } from '../../lib/breadcrumbs';
+import { resetWorkpad } from '../../state/actions/workpad';
+import { Home as Component } from './home.component';
+import { usePlatformService } from '../../services';
+
+export const Home = () => {
+ const { setBreadcrumbs } = usePlatformService();
+ const [isMounted, setIsMounted] = useState(false);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (!isMounted) {
+ dispatch(resetWorkpad());
+ setIsMounted(true);
+ }
+ }, [dispatch, isMounted, setIsMounted]);
+
+ useEffect(() => {
+ setBreadcrumbs([getBaseBreadcrumb()]);
+ }, [setBreadcrumbs]);
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/index.ts b/x-pack/plugins/canvas/public/components/home/hooks/index.ts
new file mode 100644
index 0000000000000..91e52948a7ba6
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { useCloneWorkpad } from './use_clone_workpad';
+export { useCreateWorkpad } from './use_create_workpad';
+export { useDeleteWorkpads } from './use_delete_workpad';
+export { useDownloadWorkpad } from './use_download_workpad';
+export { useFindTemplates, useFindTemplatesOnMount } from './use_find_templates';
+export { useFindWorkpads, useFindWorkpadsOnMount } from './use_find_workpad';
+export { useImportWorkpad } from './use_upload_workpad';
+export { useCreateFromTemplate } from './use_create_from_template';
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts
new file mode 100644
index 0000000000000..001a711a58a72
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+
+import { useNotifyService, useWorkpadService } from '../../../services';
+import { getId } from '../../../lib/get_id';
+
+export const useCloneWorkpad = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+ const history = useHistory();
+
+ return useCallback(
+ async (workpadId: string) => {
+ try {
+ let workpad = await workpadService.get(workpadId);
+
+ workpad = {
+ ...workpad,
+ name: strings.getClonedWorkpadName(workpad.name),
+ id: getId('workpad'),
+ };
+
+ await workpadService.create(workpad);
+
+ history.push(`/workpad/${workpad.id}/page/1`);
+ } catch (err) {
+ notifyService.error(err, { title: errors.getCloneFailureErrorMessage() });
+ }
+ },
+ [notifyService, workpadService, history]
+ );
+};
+
+const strings = {
+ getClonedWorkpadName: (workpadName: string) =>
+ i18n.translate('xpack.canvas.useCloneWorkpad.clonedWorkpadName', {
+ defaultMessage: 'Copy of {workpadName}',
+ values: {
+ workpadName,
+ },
+ description:
+ 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' +
+ 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"',
+ }),
+};
+
+const errors = {
+ getCloneFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage', {
+ defaultMessage: `Couldn't clone workpad`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts
new file mode 100644
index 0000000000000..968f9398ba857
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+
+import { CanvasTemplate } from '../../../../types';
+import { useNotifyService, useWorkpadService } from '../../../services';
+
+export const useCreateFromTemplate = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+ const history = useHistory();
+
+ return useCallback(
+ async (template: CanvasTemplate) => {
+ try {
+ const result = await workpadService.createFromTemplate(template.id);
+ history.push(`/workpad/${result.id}/page/1`);
+ } catch (e) {
+ notifyService.error(e, {
+ title: `Couldn't create workpad from template`,
+ });
+ }
+ },
+ [workpadService, notifyService, history]
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts
new file mode 100644
index 0000000000000..eb87f4720deec
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+
+// @ts-expect-error
+import { getDefaultWorkpad } from '../../../state/defaults';
+import { useNotifyService, useWorkpadService } from '../../../services';
+
+import type { CanvasWorkpad } from '../../../../types';
+
+export const useCreateWorkpad = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+ const history = useHistory();
+
+ return useCallback(
+ async (_workpad?: CanvasWorkpad | null) => {
+ const workpad = _workpad || (getDefaultWorkpad() as CanvasWorkpad);
+
+ try {
+ await workpadService.create(workpad);
+ history.push(`/workpad/${workpad.id}/page/1`);
+ } catch (err) {
+ notifyService.error(err, {
+ title: errors.getUploadFailureErrorMessage(),
+ });
+ }
+ return;
+ },
+ [notifyService, history, workpadService]
+ );
+};
+
+const errors = {
+ getUploadFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', {
+ defaultMessage: `Couldn't upload workpad`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts
new file mode 100644
index 0000000000000..722ddae7411c9
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { useNotifyService, useWorkpadService } from '../../../services';
+
+export const useDeleteWorkpads = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+
+ return useCallback(
+ async (workpadIds: string[]) => {
+ const removedWorkpads = workpadIds.map(async (id) => {
+ try {
+ await workpadService.remove(id);
+ return { id, err: null };
+ } catch (err) {
+ return { id, err };
+ }
+ });
+
+ return Promise.all(removedWorkpads).then((results) => {
+ const [passes, errored] = results.reduce<[string[], string[]]>(
+ ([passesArr, errorsArr], result) => {
+ if (result.err) {
+ errorsArr.push(result.id);
+ } else {
+ passesArr.push(result.id);
+ }
+
+ return [passesArr, errorsArr];
+ },
+ [[], []]
+ );
+
+ const removedIds = workpadIds.filter((id) => passes.includes(id));
+
+ if (errored.length > 0) {
+ notifyService.error(errors.getDeleteFailureErrorMessage());
+ }
+
+ return {
+ removedIds,
+ errored,
+ };
+ });
+ },
+ [workpadService, notifyService]
+ );
+};
+
+const errors = {
+ getDeleteFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage', {
+ defaultMessage: `Couldn't delete all workpads`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts
new file mode 100644
index 0000000000000..b875e08c2a230
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import { downloadWorkpad as downloadWorkpadFn } from '../../../lib/download_workpad';
+
+export const useDownloadWorkpad = () =>
+ useCallback((workpadId: string) => downloadWorkpadFn(workpadId), []);
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts
new file mode 100644
index 0000000000000..13ee289fe9867
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useState, useCallback } from 'react';
+import useMount from 'react-use/lib/useMount';
+
+import { useWorkpadService } from '../../../services';
+import { TemplateFindResponse } from '../../../services/workpad';
+
+const emptyResponse = { templates: [] };
+
+export const useFindTemplates = () => {
+ const workpadService = useWorkpadService();
+ return useCallback(async () => await workpadService.findTemplates(), [workpadService]);
+};
+
+export const useFindTemplatesOnMount = (): [boolean, TemplateFindResponse] => {
+ const [isMounted, setIsMounted] = useState(false);
+ const findTemplates = useFindTemplates();
+ const [templateResponse, setTemplateResponse] = useState(emptyResponse);
+
+ const fetchTemplates = useCallback(async () => {
+ const foundTemplates = await findTemplates();
+ setTemplateResponse(foundTemplates || emptyResponse);
+ setIsMounted(true);
+ }, [findTemplates]);
+
+ useMount(() => {
+ fetchTemplates();
+ return () => setIsMounted(false);
+ });
+
+ return [isMounted, templateResponse];
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts
new file mode 100644
index 0000000000000..3f8b0e6f630f5
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useState, useCallback } from 'react';
+import useMount from 'react-use/lib/useMount';
+import { i18n } from '@kbn/i18n';
+
+import { WorkpadFindResponse } from '../../../services/workpad';
+
+import { useNotifyService, useWorkpadService } from '../../../services';
+const emptyResponse = { total: 0, workpads: [] };
+
+export const useFindWorkpads = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+
+ return useCallback(
+ async (text = '') => {
+ try {
+ return await workpadService.find(text);
+ } catch (err) {
+ notifyService.error(err, { title: errors.getFindFailureErrorMessage() });
+ }
+ },
+ [notifyService, workpadService]
+ );
+};
+
+export const useFindWorkpadsOnMount = (): [boolean, WorkpadFindResponse] => {
+ const [isMounted, setIsMounted] = useState(false);
+ const findWorkpads = useFindWorkpads();
+ const [workpadResponse, setWorkpadResponse] = useState(emptyResponse);
+
+ const fetchWorkpads = useCallback(async () => {
+ const foundWorkpads = await findWorkpads();
+ setWorkpadResponse(foundWorkpads || emptyResponse);
+ setIsMounted(true);
+ }, [findWorkpads]);
+
+ useMount(() => {
+ fetchWorkpads();
+ return () => setIsMounted(false);
+ });
+
+ return [isMounted, workpadResponse];
+};
+
+const errors = {
+ getFindFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useFindWorkpads.findFailureErrorMessage', {
+ defaultMessage: `Couldn't find workpad`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
new file mode 100644
index 0000000000000..7934a469bb7a2
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import { get } from 'lodash';
+import { i18n } from '@kbn/i18n';
+
+import { CANVAS, JSON as JSONString } from '../../../../i18n/constants';
+import { useNotifyService } from '../../../services';
+import { getId } from '../../../lib/get_id';
+import type { CanvasWorkpad } from '../../../../types';
+
+export const useImportWorkpad = () => {
+ const notifyService = useNotifyService();
+
+ return useCallback(
+ (file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => {
+ if (!file) {
+ onComplete();
+ return;
+ }
+
+ if (get(file, 'type') !== 'application/json') {
+ notifyService.warning(errors.getAcceptJSONOnlyErrorMessage(), {
+ title: file.name
+ ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
+ : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
+ });
+ onComplete();
+ }
+
+ // TODO: Clean up this file, this loading stuff can, and should be, abstracted
+ const reader = new FileReader();
+
+ // handle reading the uploaded file
+ reader.onload = () => {
+ try {
+ const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below.
+ workpad.id = getId('workpad');
+
+ // sanity check for workpad object
+ if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) {
+ onComplete();
+ throw new Error(errors.getMissingPropertiesErrorMessage());
+ }
+
+ onComplete(workpad);
+ } catch (e) {
+ notifyService.error(e, {
+ title: file.name
+ ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
+ : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
+ });
+ onComplete();
+ }
+ };
+
+ // read the uploaded file
+ reader.readAsText(file);
+ },
+ [notifyService]
+ );
+};
+
+const errors = {
+ getFileUploadFailureWithoutFileNameErrorMessage: () =>
+ i18n.translate(
+ 'xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage',
+ {
+ defaultMessage: `Couldn't upload file`,
+ }
+ ),
+ getFileUploadFailureWithFileNameErrorMessage: (fileName: string) =>
+ i18n.translate('xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage', {
+ defaultMessage: `Couldn't upload '{fileName}'`,
+ values: {
+ fileName,
+ },
+ }),
+ getMissingPropertiesErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage', {
+ defaultMessage:
+ 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.',
+ values: {
+ CANVAS,
+ JSON: JSONString,
+ },
+ }),
+ getAcceptJSONOnlyErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage', {
+ defaultMessage: 'Only {JSON} files are accepted',
+ values: {
+ JSON: JSONString,
+ },
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/index.js b/x-pack/plugins/canvas/public/components/home/index.ts
similarity index 83%
rename from x-pack/plugins/canvas/public/components/workpad_manager/index.js
rename to x-pack/plugins/canvas/public/components/home/index.ts
index e1f5855e762af..aeb62c3a8de78 100644
--- a/x-pack/plugins/canvas/public/components/workpad_manager/index.js
+++ b/x-pack/plugins/canvas/public/components/home/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { WorkpadManager } from './workpad_manager';
+export { Home } from './home';
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx
new file mode 100644
index 0000000000000..aef1b0625b585
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { HomeEmptyPrompt } from './empty_prompt';
+import { getDisableStoryshotsParameter } from '../../../../storybook';
+
+export default {
+ title: 'Home/Empty Prompt',
+ argTypes: {},
+ parameters: { ...getDisableStoryshotsParameter() },
+};
+
+export const EmptyPrompt = () => ;
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx
new file mode 100644
index 0000000000000..797f50ac112d0
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import { CANVAS, JSON } from '../../../../i18n/constants';
+
+export const HomeEmptyPrompt = () => (
+
+
+
+ {strings.getEmptyPromptTitle()}}
+ titleSize="m"
+ body={
+
+ {strings.getEmptyPromptGettingStartedDescription()}
+
+ {strings.getEmptyPromptNewUserDescription()}{' '}
+
+ {strings.getSampleDataLinkLabel()}
+
+ .
+
+
+ }
+ />
+
+
+
+);
+
+const strings = {
+ getEmptyPromptGettingStartedDescription: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription', {
+ defaultMessage:
+ 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.',
+ values: {
+ JSON,
+ },
+ }),
+ getEmptyPromptNewUserDescription: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription', {
+ defaultMessage: 'New to {CANVAS}?',
+ values: {
+ CANVAS,
+ },
+ }),
+ getEmptyPromptTitle: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptTitle', {
+ defaultMessage: 'Add your first workpad',
+ }),
+ getSampleDataLinkLabel: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel', {
+ defaultMessage: 'Add your first workpad',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts b/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts
new file mode 100644
index 0000000000000..79b1519df90fe
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+export const LazyMyWorkpads = React.lazy(() => import('./my_workpads'));
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx
new file mode 100644
index 0000000000000..28edfea7c36ca
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+export const Loading = () => (
+
+
+
+
+
+);
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx
new file mode 100644
index 0000000000000..d9e3f0e4e2c99
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import { FoundWorkpad } from '../../../services/workpad';
+import { UploadDropzone } from './upload_dropzone';
+import { HomeEmptyPrompt } from './empty_prompt';
+import { WorkpadTable } from './workpad_table';
+
+export interface Props {
+ workpads: FoundWorkpad[];
+}
+
+export const MyWorkpads = ({ workpads }: Props) => {
+ if (workpads.length === 0) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx
new file mode 100644
index 0000000000000..0d5d6ca16f614
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { EuiPanel } from '@elastic/eui';
+
+import {
+ reduxDecorator,
+ getAddonPanelParameters,
+ servicesContextDecorator,
+ getDisableStoryshotsParameter,
+} from '../../../../storybook';
+import { getSomeWorkpads } from '../../../services/stubs/workpad';
+
+import { MyWorkpads, WorkpadsContext } from './my_workpads';
+import { MyWorkpads as MyWorkpadsComponent } from './my_workpads.component';
+
+export default {
+ title: 'Home/My Workpads',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoWorkpads = () => {
+ return ;
+};
+
+export const HasWorkpads = () => {
+ return (
+
+
+
+ );
+};
+
+NoWorkpads.decorators = [servicesContextDecorator()];
+HasWorkpads.decorators = [servicesContextDecorator({ findWorkpads: 5 })];
+
+export const Component = ({ workpadCount }: { workpadCount: number }) => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount));
+
+ return (
+
+
+
+
+
+ );
+};
+
+Component.args = { workpadCount: 5 };
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx
new file mode 100644
index 0000000000000..4242e2e9d130f
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, useEffect, createContext, Dispatch, SetStateAction } from 'react';
+import { useFindWorkpadsOnMount } from './../hooks';
+import { FoundWorkpad } from '../../../services/workpad';
+import { Loading } from './loading';
+import { MyWorkpads as Component } from './my_workpads.component';
+
+interface Context {
+ workpads: FoundWorkpad[];
+ setWorkpads: Dispatch>;
+}
+
+export const WorkpadsContext = createContext(null);
+
+export const MyWorkpads = () => {
+ const [isMounted, workpadResponse] = useFindWorkpadsOnMount();
+ const [workpads, setWorkpads] = useState(workpadResponse.workpads);
+
+ useEffect(() => {
+ setWorkpads(workpadResponse.workpads);
+ }, [workpadResponse]);
+
+ if (!isMounted) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
+
+// required for dynamic import using React.lazy()
+// eslint-disable-next-line import/no-default-export
+export default MyWorkpads;
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx
new file mode 100644
index 0000000000000..603f4679a9e95
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+// @ts-expect-error untyped library
+import Dropzone from 'react-dropzone';
+
+import './upload_dropzone.scss';
+
+export interface Props {
+ disabled?: boolean;
+ onDrop?: (files: FileList) => void;
+}
+
+export const UploadDropzone: FC = ({ onDrop = () => {}, disabled, children }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss
new file mode 100644
index 0000000000000..e4ee284c72dee
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss
@@ -0,0 +1,8 @@
+.canvasWorkpad__dropzone {
+ border: 2px dashed transparent;
+}
+
+.canvasWorkpad__dropzone--active {
+ background-color: $euiColorLightestShade;
+ border-color: $euiColorLightShade;
+}
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx
new file mode 100644
index 0000000000000..8ee0ae108392e
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useState } from 'react';
+// @ts-expect-error untyped library
+import Dropzone from 'react-dropzone';
+
+import { useNotifyService } from '../../../services';
+import { ErrorStrings } from '../../../../i18n';
+import { useImportWorkpad, useCreateWorkpad } from '../hooks';
+import { CanvasWorkpad } from '../../../../types';
+
+import { UploadDropzone as Component } from './upload_dropzone.component';
+
+const { WorkpadDropzone: errors } = ErrorStrings;
+
+export const UploadDropzone: FC = ({ children }) => {
+ const notify = useNotifyService();
+ const uploadWorkpad = useImportWorkpad();
+ const createWorkpad = useCreateWorkpad();
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ const onComplete = async (workpad?: CanvasWorkpad) => {
+ if (!workpad) {
+ setIsDisabled(false);
+ return;
+ }
+
+ await createWorkpad(workpad);
+ };
+
+ const onDrop = (files: FileList) => {
+ if (!files) {
+ return;
+ }
+
+ if (files.length > 1) {
+ notify.warning(errors.getTooManyFilesErrorMessage());
+ return;
+ }
+
+ setIsDisabled(true);
+ uploadWorkpad(files[0], onComplete);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx
new file mode 100644
index 0000000000000..28e2aa0449d46
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiFilePicker, EuiFilePickerProps } from '@elastic/eui';
+
+import { JSON } from '../../../../i18n/constants';
+export interface Props {
+ canUserWrite: boolean;
+ onImportWorkpad?: EuiFilePickerProps['onChange'];
+ uniqueKey?: string | number;
+}
+
+export const WorkpadImport = ({ uniqueKey, canUserWrite, onImportWorkpad = () => {} }: Props) => (
+
+);
+
+const strings = {
+ getFilePickerPlaceholder: () =>
+ i18n.translate('xpack.canvas.workpadImport.filePickerPlaceholder', {
+ defaultMessage: 'Import workpad {JSON} file',
+ values: {
+ JSON,
+ },
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx
new file mode 100644
index 0000000000000..0f1ba621e14d7
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
+import type { State } from '../../../../types';
+
+import { useImportWorkpad } from '../hooks';
+import { WorkpadImport as Component, Props as ComponentProps } from './workpad_import.component';
+
+type Props = Omit;
+
+export const WorkpadImport = (props: Props) => {
+ const importWorkpad = useImportWorkpad();
+ const [uniqueKey, setUniqueKey] = useState(Date.now());
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ const onImportWorkpad: ComponentProps['onImportWorkpad'] = (files) => {
+ if (files) {
+ importWorkpad(files[0]);
+ }
+ setUniqueKey(Date.now());
+ };
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx
new file mode 100644
index 0000000000000..5301a88844369
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx
@@ -0,0 +1,203 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiInMemoryTable,
+ EuiInMemoryTableProps,
+ EuiTableActionsColumnType,
+ EuiBasicTableColumn,
+ EuiToolTip,
+ EuiButtonIcon,
+ EuiTableSelectionType,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+import moment from 'moment';
+
+import { RoutingLink } from '../../routing';
+import { FoundWorkpad } from '../../../services/workpad';
+import { WorkpadTableTools } from './workpad_table_tools';
+import { WorkpadImport } from './workpad_import';
+
+export interface Props {
+ workpads: FoundWorkpad[];
+ canUserWrite: boolean;
+ dateFormat: string;
+ onExportWorkpad: (ids: string) => void;
+ onCloneWorkpad: (id: string) => void;
+}
+
+const getDisplayName = (name: string, workpadId: string, loadedWorkpadId?: string) => {
+ const workpadName = name.length ? {name} : {workpadId} ;
+ return workpadId === loadedWorkpadId ? {workpadName} : workpadName;
+};
+
+export const WorkpadTable = ({
+ workpads,
+ canUserWrite,
+ dateFormat,
+ onExportWorkpad: onExport,
+ onCloneWorkpad,
+}: Props) => {
+ const [selectedIds, setSelectedIds] = useState([]);
+ const formatDate = (date: string) => date && moment(date).format(dateFormat);
+
+ const selection: EuiTableSelectionType = {
+ onSelectionChange: (selectedWorkpads) => {
+ setSelectedIds(selectedWorkpads.map((workpad) => workpad.id).filter((id) => !!id));
+ },
+ };
+
+ const actions: EuiTableActionsColumnType['actions'] = [
+ {
+ render: (workpad: FoundWorkpad) => (
+
+
+
+ onExport(workpad.id)}
+ aria-label={strings.getExportToolTip()}
+ />
+
+
+
+
+ onCloneWorkpad(workpad.id)}
+ aria-label={strings.getCloneToolTip()}
+ disabled={!canUserWrite}
+ />
+
+
+
+ ),
+ },
+ ];
+
+ const search: EuiInMemoryTableProps['search'] = {
+ toolsLeft:
+ selectedIds.length > 0 ? : undefined,
+ toolsRight: ,
+ box: {
+ schema: true,
+ incremental: true,
+ placeholder: strings.getWorkpadSearchPlaceholder(),
+ 'data-test-subj': 'tableListSearchBox',
+ },
+ };
+
+ const columns: Array> = [
+ {
+ field: 'name',
+ name: strings.getTableNameColumnTitle(),
+ sortable: true,
+ dataType: 'string',
+ render: (name, workpad) => (
+
+ {getDisplayName(name, workpad.id)}
+
+ ),
+ },
+ {
+ field: '@created',
+ name: strings.getTableCreatedColumnTitle(),
+ sortable: true,
+ dataType: 'date',
+ width: '20%',
+ render: (date: string) => formatDate(date),
+ },
+ {
+ field: '@timestamp',
+ name: strings.getTableUpdatedColumnTitle(),
+ sortable: true,
+ dataType: 'date',
+ width: '20%',
+ render: (date: string) => formatDate(date),
+ },
+ { name: strings.getTableActionsColumnTitle(), actions, width: '100px' },
+ ];
+
+ return (
+
+ );
+};
+
+const strings = {
+ getCloneToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTable.cloneTooltip', {
+ defaultMessage: 'Clone workpad',
+ }),
+ getExportToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTable.exportTooltip', {
+ defaultMessage: 'Export workpad',
+ }),
+ getLoadWorkpadArialLabel: (workpadName: string) =>
+ i18n.translate('xpack.canvas.workpadTable.loadWorkpadArialLabel', {
+ defaultMessage: `Load workpad '{workpadName}'`,
+ values: {
+ workpadName,
+ },
+ }),
+ getNoPermissionToCloneToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTable.noPermissionToCloneToolTip', {
+ defaultMessage: `You don't have permission to clone workpads`,
+ }),
+ getNoWorkpadsFoundMessage: () =>
+ i18n.translate('xpack.canvas.workpadTable.noWorkpadsFoundMessage', {
+ defaultMessage: 'No workpads matched your search.',
+ }),
+ getWorkpadSearchPlaceholder: () =>
+ i18n.translate('xpack.canvas.workpadTable.searchPlaceholder', {
+ defaultMessage: 'Find workpad',
+ }),
+ getTableCreatedColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.createdColumnTitle', {
+ defaultMessage: 'Created',
+ description: 'This column in the table contains the date/time the workpad was created.',
+ }),
+ getTableNameColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.nameColumnTitle', {
+ defaultMessage: 'Workpad name',
+ }),
+ getTableUpdatedColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.updatedColumnTitle', {
+ defaultMessage: 'Updated',
+ description: 'This column in the table contains the date/time the workpad was last updated.',
+ }),
+ getTableActionsColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.actionsColumnTitle', {
+ defaultMessage: 'Actions',
+ description: 'This column in the table contains the actions that can be taken on a workpad.',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx
new file mode 100644
index 0000000000000..501a0a76a8589
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, useEffect } from 'react';
+import { EuiPanel } from '@elastic/eui';
+
+import { action } from '@storybook/addon-actions';
+import {
+ reduxDecorator,
+ getAddonPanelParameters,
+ getDisableStoryshotsParameter,
+} from '../../../../storybook';
+import { getSomeWorkpads } from '../../../services/stubs/workpad';
+
+import { WorkpadTable } from './workpad_table';
+import { WorkpadTable as WorkpadTableComponent } from './workpad_table.component';
+import { WorkpadsContext } from './my_workpads';
+
+export default {
+ title: 'Home/Workpad Table',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoWorkpads = () => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(0));
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const HasWorkpads = () => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(5));
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const Component = ({
+ workpadCount,
+ canUserWrite,
+ dateFormat,
+}: {
+ workpadCount: number;
+ canUserWrite: boolean;
+ dateFormat: string;
+}) => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount));
+
+ useEffect(() => {
+ setWorkpads(getSomeWorkpads(workpadCount));
+ }, [workpadCount]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+Component.args = { workpadCount: 5, canUserWrite: true, dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS' };
+Component.argTypes = {};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
new file mode 100644
index 0000000000000..e5d83039a87eb
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useContext } from 'react';
+import { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
+import type { State } from '../../../../types';
+import { usePlatformService } from '../../../services';
+import { useCloneWorkpad, useDownloadWorkpad } from '../hooks';
+
+import { WorkpadTable as Component } from './workpad_table.component';
+import { WorkpadsContext } from './my_workpads';
+
+export const WorkpadTable = () => {
+ const platformService = usePlatformService();
+ const onCloneWorkpad = useCloneWorkpad();
+ const onExportWorkpad = useDownloadWorkpad();
+ const context = useContext(WorkpadsContext);
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ if (!context) {
+ return null;
+ }
+
+ const { workpads } = context;
+
+ const dateFormat = platformService.getUISetting('dateFormat');
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx
new file mode 100644
index 0000000000000..ae6ff9c3cc910
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState, Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButton, EuiToolTip, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
+
+import { ConfirmModal } from '../../confirm_modal';
+import { FoundWorkpad } from '../../../services/workpad';
+
+export interface Props {
+ workpads: FoundWorkpad[];
+ canUserWrite: boolean;
+ selectedWorkpadIds: string[];
+ onDeleteWorkpads: (ids: string[]) => void;
+ onExportWorkpads: (ids: string[]) => void;
+}
+
+export const WorkpadTableTools = ({
+ workpads,
+ canUserWrite,
+ selectedWorkpadIds,
+ onDeleteWorkpads,
+ onExportWorkpads,
+}: Props) => {
+ const [isDeletePending, setIsDeletePending] = useState(false);
+
+ const openRemoveConfirm = () => setIsDeletePending(true);
+ const closeRemoveConfirm = () => setIsDeletePending(false);
+
+ let deleteButton = (
+
+ {strings.getDeleteButtonLabel(selectedWorkpadIds.length)}
+
+ );
+
+ const downloadButton = (
+ onExportWorkpads(selectedWorkpadIds)}
+ iconType="exportAction"
+ aria-label={strings.getExportButtonAriaLabel(selectedWorkpadIds.length)}
+ >
+ {strings.getExportButtonLabel(selectedWorkpadIds.length)}
+
+ );
+
+ if (!canUserWrite) {
+ deleteButton = (
+ {deleteButton}
+ );
+ }
+
+ const modalTitle =
+ selectedWorkpadIds.length === 1
+ ? strings.getDeleteSingleWorkpadModalTitle(
+ workpads.find((workpad) => workpad.id === selectedWorkpadIds[0])?.name || ''
+ )
+ : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpadIds.length + '');
+
+ const confirmModal = (
+ {
+ onDeleteWorkpads(selectedWorkpadIds);
+ closeRemoveConfirm();
+ }}
+ onCancel={closeRemoveConfirm}
+ />
+ );
+
+ return (
+
+
+ {downloadButton}
+ {deleteButton}
+
+ {confirmModal}
+
+ );
+};
+
+const strings = {
+ getDeleteButtonAriaLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteButtonAriaLabel', {
+ defaultMessage: 'Delete {numberOfWorkpads} workpads',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getDeleteButtonLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteButtonLabel', {
+ defaultMessage: 'Delete ({numberOfWorkpads})',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getDeleteModalConfirmButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel', {
+ defaultMessage: 'Delete',
+ }),
+ getDeleteModalDescription: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteModalDescription', {
+ defaultMessage: `You can't recover deleted workpads.`,
+ }),
+ getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle', {
+ defaultMessage: 'Delete {numberOfWorkpads} workpads?',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getDeleteSingleWorkpadModalTitle: (workpadName: string) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle', {
+ defaultMessage: `Delete workpad '{workpadName}'?`,
+ values: {
+ workpadName,
+ },
+ }),
+ getExportButtonAriaLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.exportButtonAriaLabel', {
+ defaultMessage: 'Export {numberOfWorkpads} workpads',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getExportButtonLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.exportButtonLabel', {
+ defaultMessage: 'Export ({numberOfWorkpads})',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getNoPermissionToCreateToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.noPermissionToCreateToolTip', {
+ defaultMessage: `You don't have permission to create workpads`,
+ }),
+ getNoPermissionToDeleteToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip', {
+ defaultMessage: `You don't have permission to delete workpads`,
+ }),
+ getNoPermissionToUploadToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.noPermissionToUploadToolTip', {
+ defaultMessage: `You don't have permission to upload workpads`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
new file mode 100644
index 0000000000000..62d84adfc2649
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useContext } from 'react';
+import { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
+import type { State } from '../../../../types';
+import { useDeleteWorkpads, useDownloadWorkpad } from '../hooks';
+
+import {
+ WorkpadTableTools as Component,
+ Props as ComponentProps,
+} from './workpad_table_tools.component';
+import { WorkpadsContext } from './my_workpads';
+
+export type Props = Pick;
+
+export const WorkpadTableTools = ({ selectedWorkpadIds }: Props) => {
+ const deleteWorkpads = useDeleteWorkpads();
+ const downloadWorkpad = useDownloadWorkpad();
+ const context = useContext(WorkpadsContext);
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ if (context === null || selectedWorkpadIds.length <= 0) {
+ return null;
+ }
+
+ const { workpads, setWorkpads } = context;
+
+ const onExport = () => selectedWorkpadIds.map((id) => downloadWorkpad(id));
+ const onDelete = async () => {
+ const { removedIds } = await deleteWorkpads(selectedWorkpadIds);
+ setWorkpads(workpads.filter((workpad) => !removedIds.includes(workpad.id)));
+ };
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx
new file mode 100644
index 0000000000000..18bdb97683194
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButton } from '@elastic/eui';
+import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button';
+
+export interface Props
+ extends Omit {
+ canUserWrite: boolean;
+}
+
+export const WorkpadCreate = ({ canUserWrite, disabled, ...rest }: Props) => {
+ return (
+
+ {strings.getWorkpadCreateButtonLabel()}
+
+ );
+};
+
+const strings = {
+ getWorkpadCreateButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', {
+ defaultMessage: 'Create workpad',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.tsx
new file mode 100644
index 0000000000000..adb73a6bb8896
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_create.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app';
+import type { State } from '../../../types';
+
+import { useCreateWorkpad } from './hooks';
+import { WorkpadCreate as Component, Props as ComponentProps } from './workpad_create.component';
+
+type Props = Omit;
+
+export const WorkpadCreate = (props: Props) => {
+ const createWorkpad = useCreateWorkpad();
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ const onClick: ComponentProps['onClick'] = async () => {
+ await createWorkpad();
+ };
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts
new file mode 100644
index 0000000000000..4c45dbff38377
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+export const LazyWorkpadTemplates = React.lazy(() => import('./workpad_templates'));
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx
new file mode 100644
index 0000000000000..d974c70b05cf2
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { uniq } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiInMemoryTable,
+ EuiBasicTableColumn,
+ EuiButtonEmpty,
+ EuiSearchBarProps,
+ SearchFilterConfig,
+} from '@elastic/eui';
+
+import { CanvasTemplate } from '../../../../types';
+import { tagsRegistry } from '../../../lib/tags_registry';
+import { TagList } from '../../tag_list';
+
+export interface Props {
+ templates: CanvasTemplate[];
+ onCreateWorkpad: (template: CanvasTemplate) => void;
+}
+
+export const WorkpadTemplates = ({ templates, onCreateWorkpad }: Props) => {
+ const columns: Array> = [
+ {
+ field: 'name',
+ name: strings.getTableNameColumnTitle(),
+ sortable: true,
+ width: '30%',
+ dataType: 'string',
+ render: (name: string, template) => {
+ const templateName = name.length ? name : 'Unnamed Template';
+
+ return (
+ onCreateWorkpad(template)}
+ aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)}
+ type="button"
+ >
+ {templateName}
+
+ );
+ },
+ },
+ {
+ field: 'help',
+ name: strings.getTableDescriptionColumnTitle(),
+ sortable: false,
+ dataType: 'string',
+ width: '30%',
+ },
+ {
+ field: 'tags',
+ name: strings.getTableTagsColumnTitle(),
+ sortable: false,
+ dataType: 'string',
+ width: '30%',
+ render: (tags: string[]) => ,
+ },
+ ];
+
+ let uniqueTagNames: string[] = [];
+
+ templates.forEach((template) => {
+ const { tags } = template;
+ tags.forEach((tag) => uniqueTagNames.push(tag));
+ uniqueTagNames = uniq(uniqueTagNames);
+ });
+
+ const uniqueTags = uniqueTagNames.map(
+ (name) =>
+ tagsRegistry.get(name) || {
+ color: undefined,
+ name,
+ }
+ );
+
+ const filters: SearchFilterConfig[] = [
+ {
+ type: 'field_value_selection',
+ field: 'tags',
+ name: 'Tags',
+ multiSelect: true,
+ options: uniqueTags.map((tag) => ({
+ value: tag.name,
+ name: tag.name,
+ view: ,
+ })),
+ },
+ ];
+
+ const search: EuiSearchBarProps = {
+ box: {
+ incremental: true,
+ schema: true,
+ },
+ filters,
+ };
+
+ return (
+
+ );
+};
+
+const strings = {
+ getCloneTemplateLinkAriaLabel: (templateName: string) =>
+ i18n.translate('xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel', {
+ defaultMessage: `Clone workpad template '{templateName}'`,
+ values: {
+ templateName,
+ },
+ }),
+ getTableDescriptionColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', {
+ defaultMessage: 'Description',
+ }),
+ getTableNameColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', {
+ defaultMessage: 'Template name',
+ }),
+ getTableTagsColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', {
+ defaultMessage: 'Tags',
+ description:
+ 'This column contains relevant tags that indicate what type of template ' +
+ 'is displayed. For example: "report", "presentation", etc.',
+ }),
+ getTemplateSearchPlaceholder: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.searchPlaceholder', {
+ defaultMessage: 'Find template',
+ }),
+ getCreatingTemplateLabel: (templateName: string) =>
+ i18n.translate('xpack.canvas.workpadTemplates.creatingTemplateLabel', {
+ defaultMessage: `Creating from template '{templateName}'`,
+ values: {
+ templateName,
+ },
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx
new file mode 100644
index 0000000000000..cb2b872ea15f9
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiPanel } from '@elastic/eui';
+import { action } from '@storybook/addon-actions';
+import React from 'react';
+
+import {
+ reduxDecorator,
+ getAddonPanelParameters,
+ servicesContextDecorator,
+ getDisableStoryshotsParameter,
+} from '../../../../storybook';
+import { getSomeTemplates } from '../../../services/stubs/workpad';
+
+import { WorkpadTemplates } from './workpad_templates';
+import { WorkpadTemplates as WorkpadTemplatesComponent } from './workpad_templates.component';
+
+export default {
+ title: 'Home/Workpad Templates',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoTemplates = () => {
+ return (
+
+
+
+ );
+};
+
+export const HasTemplates = () => {
+ return (
+
+
+
+ );
+};
+
+NoTemplates.decorators = [servicesContextDecorator()];
+HasTemplates.decorators = [servicesContextDecorator({ findTemplates: true })];
+
+export const Component = ({ hasTemplates }: { hasTemplates: boolean }) => {
+ return (
+
+
+
+ );
+};
+
+Component.args = {
+ hasTemplates: true,
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx
new file mode 100644
index 0000000000000..352285e66424b
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+
+import { useCreateFromTemplate, useFindTemplatesOnMount } from '../hooks';
+
+import { WorkpadTemplates as Component } from './workpad_templates.component';
+
+export const WorkpadTemplates = () => {
+ const [isMounted, templateResponse] = useFindTemplatesOnMount();
+ const onCreateWorkpad = useCreateFromTemplate();
+
+ if (!isMounted) {
+ return (
+
+
+
+
+
+ );
+ }
+ const { templates } = templateResponse;
+
+ return ;
+};
+
+// required for dynamic import using React.lazy()
+// eslint-disable-next-line import/no-default-export
+export default WorkpadTemplates;
diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx
index 712b06cb39299..2e3e826cc32b5 100644
--- a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx
+++ b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx
@@ -6,9 +6,7 @@
*/
import React, { FC } from 'react';
-import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
-// @ts-expect-error untyped local
-import { WorkpadManager } from '../workpad_manager';
+import { Home } from '../home';
// @ts-expect-error untyped local
import { setDocTitle } from '../../lib/doc_title';
@@ -19,17 +17,5 @@ export interface Props {
export const HomeApp: FC = ({ onLoad = () => {} }) => {
onLoad();
setDocTitle('Canvas');
- return (
-
-
-
- {}} />
-
-
-
- );
+ return ;
};
diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot
index 5e833944046a4..bc6430c4c0357 100644
--- a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot
@@ -2,1103 +2,1088 @@
exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = `
+
-
-
-
-
-
- Keyboard shortcuts
-
-
+ Keyboard shortcuts
+
+
+
-
+ Element controls
+
+
+
-
- Element controls
-
-
-
-
- Cut
-
-
-
-
-
- CTRL
-
-
-
-
-
- X
-
-
-
-
-
- Copy
-
-
-
-
-
- CTRL
-
-
-
-
-
- C
-
-
-
-
-
- Paste
-
-
-
-
-
- CTRL
-
-
-
-
-
- V
-
-
-
-
-
- Clone
-
-
-
-
-
- CTRL
-
-
-
-
-
- D
-
-
-
-
-
- Delete
-
-
-
-
-
- DEL
-
-
-
-
-
- or
-
-
-
-
-
- BACKSPACE
-
-
-
-
-
- Bring to front
-
-
-
-
-
- CTRL
-
-
-
-
-
- ↑
-
-
-
-
-
- Bring forward
-
-
-
-
-
- CTRL
-
-
-
-
-
- SHIFT
-
-
-
-
-
- ↑
-
-
-
-
-
- Send backward
-
-
-
-
-
- CTRL
-
-
-
-
-
- ↓
-
-
-
-
-
- Send to back
-
-
-
-
-
- CTRL
-
-
-
-
-
- SHIFT
-
-
-
-
-
- ↓
-
-
-
-
-
- Group
-
-
-
-
-
- G
-
-
-
-
-
- Ungroup
-
-
-
-
-
- U
-
-
-
-
-
- Shift up by 10px
-
-
-
-
-
- ↑
-
-
-
-
-
- Shift down by 10px
-
-
-
-
-
- ↓
-
-
-
-
-
- Shift left by 10px
-
-
-
-
-
- ←
-
-
-
-
-
- Shift right by 10px
-
-
-
-
-
- →
-
-
-
-
-
- Shift up by 1px
-
-
-
-
-
- SHIFT
-
-
-
-
-
- ↑
-
-
-
-
-
- Shift down by 1px
-
-
-
-
-
- SHIFT
-
-
-
-
-
- ↓
-
-
-
-
-
- Shift left by 1px
-
-
-
-
-
- SHIFT
-
-
-
-
-
- ←
-
-
-
-
-
- Shift right by 1px
-
-
-
-
-
- SHIFT
-
-
-
-
-
- →
-
-
-
-
-
-
-
+
+ Cut
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ X
+
+
+
+
+
+ Copy
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ C
+
+
+
+
+
+ Paste
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ V
+
+
+
+
+
+ Clone
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ D
+
+
+
+
+
+ Delete
+
+
+
+
+
+ DEL
+
+
+
+
+
+ or
+
+
+
+
+
+ BACKSPACE
+
+
+
+
+
+ Bring to front
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ ↑
+
+
+
+
+
+ Bring forward
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ ↑
+
+
+
+
+
+ Send backward
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ ↓
+
+
+
+
+
+ Send to back
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ ↓
+
+
+
+
+
+ Group
+
+
+
+
+
+ G
+
+
+
+
+
+ Ungroup
+
+
+
+
+
+ U
+
+
+
+
+
+ Shift up by 10px
+
+
+
+
+
+ ↑
+
+
+
+
+
+ Shift down by 10px
+
+
+
+
+
+ ↓
+
+
+
+
+
+ Shift left by 10px
+
+
+
+
+
+ ←
+
+
+
+
+
+ Shift right by 10px
+
+
+
+
+
+ →
+
+
+
+
+
+ Shift up by 1px
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ ↑
+
+
+
+
+
+ Shift down by 1px
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ ↓
+
+
+
+
+
+ Shift left by 1px
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ ←
+
+
+
+
+
+ Shift right by 1px
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ →
+
+
+
+
+
+
+
+
+ Expression controls
+
+
+
-
- Expression controls
-
-
-
-
- Run whole expression
-
-
-
-
-
- CTRL
-
-
-
-
-
- ENTER
-
-
-
-
-
-
-
+
+ Run whole expression
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ ENTER
+
+
+
+
+
+
+
+
-
- Editor controls
-
-
-
-
- Select multiple elements
-
-
-
-
-
- SHIFT
-
-
-
-
-
- CLICK
-
-
-
-
-
- Resize from center
-
-
-
-
-
- ALT
-
-
-
-
-
- DRAG
-
-
-
-
-
- Move, resize, and rotate without snapping
-
-
-
-
-
- CTRL
-
-
-
-
-
- DRAG
-
-
-
-
-
- Select element below
-
-
-
-
-
- CTRL
-
-
-
-
-
- CLICK
-
-
-
-
-
- Undo last action
-
-
-
-
-
- CTRL
-
-
-
-
-
- Z
-
-
-
-
-
- Redo last action
-
-
-
-
-
- CTRL
-
-
-
-
-
- SHIFT
-
-
-
-
-
- Z
-
-
-
-
-
- Go to previous page
-
-
-
-
-
- ALT
-
-
-
-
-
- [
-
-
-
-
-
- Go to next page
-
-
-
-
-
- ALT
-
-
-
-
-
- ]
-
-
-
-
-
- Toggle edit mode
-
-
-
-
-
- ALT
-
-
-
-
-
- E
-
-
-
-
-
- Show grid
-
-
-
-
-
- ALT
-
-
-
-
-
- G
-
-
-
-
-
- Refresh workpad
-
-
-
-
-
- ALT
-
-
-
-
-
- R
-
-
-
-
-
- Zoom in
-
-
-
-
-
- CTRL
-
-
-
-
-
- ALT
-
-
-
-
-
- +
-
-
-
-
-
- Zoom out
-
-
-
-
-
- CTRL
-
-
-
-
-
- ALT
-
-
-
-
-
- -
-
-
-
-
-
- Reset zoom to 100%
-
-
-
-
-
- CTRL
-
-
-
-
-
- ALT
-
-
-
-
-
- [
-
-
-
-
-
- Enter presentation mode
-
-
-
-
-
- ALT
-
-
-
-
-
- F
-
-
-
-
-
- or
-
-
-
-
-
- ALT
-
-
-
-
-
- P
-
-
-
-
-
-
-
+ Editor controls
+
+
+
+
+ Select multiple elements
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ CLICK
+
+
+
+
+
+ Resize from center
+
+
+
+
+
+ ALT
+
+
+
+
+
+ DRAG
+
+
+
+
+
+ Move, resize, and rotate without snapping
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ DRAG
+
+
+
+
+
+ Select element below
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ CLICK
+
+
+
+
+
+ Undo last action
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ Z
+
+
+
+
+
+ Redo last action
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ SHIFT
+
+
+
+
+
+ Z
+
+
+
+
+
+ Go to previous page
+
+
+
+
+
+ ALT
+
+
+
+
+
+ [
+
+
+
+
+
+ Go to next page
+
+
+
+
+
+ ALT
+
+
+
+
+
+ ]
+
+
+
+
+
+ Toggle edit mode
+
+
+
+
+
+ ALT
+
+
+
+
+
+ E
+
+
+
+
+
+ Show grid
+
+
+
+
+
+ ALT
+
+
+
+
+
+ G
+
+
+
+
+
+ Refresh workpad
+
+
+
+
+
+ ALT
+
+
+
+
+
+ R
+
+
+
+
+
+ Zoom in
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ ALT
+
+
+
+
+
+ +
+
+
+
+
+
+ Zoom out
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ ALT
+
+
+
+
+
+ -
+
+
+
+
+
+ Reset zoom to 100%
+
+
+
+
+
+ CTRL
+
+
+
+
+
+ ALT
+
+
+
+
+
+ [
+
+
+
+
+
+ Enter presentation mode
+
+
+
+
+
+ ALT
+
+
+
+
+
+ F
+
+
+
+
+
+ or
+
+
+
+
+
+ ALT
+
+
+
+
+
+ P
+
+
+
+
+
+
+
+
+ Presentation controls
+
+
+
-
- Presentation controls
-
-
-
-
- Enter presentation mode
-
-
-
-
-
- ALT
-
-
-
-
-
- F
-
-
-
-
-
- or
-
-
-
-
-
- ALT
-
-
-
-
-
- P
-
-
-
-
-
- Exit presentation mode
-
-
-
-
-
- ESC
-
-
-
-
-
- Go to previous page
-
-
-
-
-
- ALT
-
-
-
-
-
- [
-
-
-
-
-
- or
-
-
-
-
-
- BACKSPACE
-
-
-
-
-
- or
-
-
-
-
-
- ←
-
-
-
-
-
- Go to next page
-
-
-
-
-
- ALT
-
-
-
-
-
- ]
-
-
-
-
-
- or
-
-
-
-
-
- SPACE
-
-
-
-
-
- or
-
-
-
-
-
- →
-
-
-
-
-
- Refresh workpad
-
-
-
-
-
- ALT
-
-
-
-
-
- R
-
-
-
-
-
- Toggle page cycling
-
-
-
-
-
- P
-
-
-
-
-
-
-
+
+ Enter presentation mode
+
+
+
+
+
+ ALT
+
+
+
+
+
+ F
+
+
+
+
+
+ or
+
+
+
+
+
+ ALT
+
+
+
+
+
+ P
+
+
+
+
+
+ Exit presentation mode
+
+
+
+
+
+ ESC
+
+
+
+
+
+ Go to previous page
+
+
+
+
+
+ ALT
+
+
+
+
+
+ [
+
+
+
+
+
+ or
+
+
+
+
+
+ BACKSPACE
+
+
+
+
+
+ or
+
+
+
+
+
+ ←
+
+
+
+
+
+ Go to next page
+
+
+
+
+
+ ALT
+
+
+
+
+
+ ]
+
+
+
+
+
+ or
+
+
+
+
+
+ SPACE
+
+
+
+
+
+ or
+
+
+
+
+
+ →
+
+
+
+
+
+ Refresh workpad
+
+
+
+
+
+ ALT
+
+
+
+
+
+ R
+
+
+
+
+
+ Toggle page cycling
+
+
+
+
+
+ P
+
+
+
+
+
+
diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx
index 0c98ea70b5b9d..a71976006d51c 100644
--- a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx
+++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx
@@ -17,14 +17,30 @@ import {
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { keymap } from '../../lib/keymap';
import { ShortcutMap, ShortcutNameSpace } from '../../../types/shortcuts';
import { getClientPlatform } from '../../lib/get_client_platform';
import { getId } from '../../lib/get_id';
import { getPrettyShortcut } from '../../lib/get_pretty_shortcut';
-import { ComponentStrings } from '../../../i18n/components';
-const { KeyboardShortcutsDoc: strings } = ComponentStrings;
+const strings = {
+ getFlyoutCloseButtonAriaLabel: () =>
+ i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel', {
+ defaultMessage: 'Closes keyboard shortcuts reference',
+ }),
+ getShortcutSeparator: () =>
+ i18n.translate('xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator', {
+ defaultMessage: 'or',
+ description:
+ 'Separates which keyboard shortcuts can be used for a single action. Example: "{shortcut1} or {shortcut2} or {shortcut3}"',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle', {
+ defaultMessage: 'Keyboard shortcuts',
+ }),
+};
interface DescriptionListItem {
title: string;
diff --git a/x-pack/plugins/canvas/public/components/page_config/index.js b/x-pack/plugins/canvas/public/components/page_config/index.js
index 59f0ac99fd73b..898ac60e68e38 100644
--- a/x-pack/plugins/canvas/public/components/page_config/index.js
+++ b/x-pack/plugins/canvas/public/components/page_config/index.js
@@ -7,13 +7,22 @@
import { connect } from 'react-redux';
import { get } from 'lodash';
+import { i18n } from '@kbn/i18n';
+
import { transitionsRegistry } from '../../lib/transitions_registry';
import { getSelectedPageIndex, getPages } from '../../state/selectors/workpad';
import { stylePage, setPageTransition } from '../../state/actions/pages';
-import { ComponentStrings } from '../../../i18n';
import { PageConfig as Component } from './page_config';
-const { PageConfig: strings } = ComponentStrings;
+const strings = {
+ getNoTransitionDropDownOptionLabel: () =>
+ i18n.translate('xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel', {
+ defaultMessage: 'None',
+ description:
+ 'This is the option the user should choose if they do not want any page transition (i.e. fade in, fade out, etc) to ' +
+ 'be applied to the current page.',
+ }),
+};
const mapStateToProps = (state) => {
const pageIndex = getSelectedPageIndex(state);
diff --git a/x-pack/plugins/canvas/public/components/page_config/page_config.js b/x-pack/plugins/canvas/public/components/page_config/page_config.js
index bc7d92de2273c..8b0c2fedf3af3 100644
--- a/x-pack/plugins/canvas/public/components/page_config/page_config.js
+++ b/x-pack/plugins/canvas/public/components/page_config/page_config.js
@@ -16,10 +16,35 @@ import {
EuiToolTip,
EuiIcon,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { WorkpadColorPicker } from '../workpad_color_picker';
-import { ComponentStrings } from '../../../i18n';
-const { PageConfig: strings } = ComponentStrings;
+const strings = {
+ getBackgroundColorDescription: () =>
+ i18n.translate('xpack.canvas.pageConfig.backgroundColorDescription', {
+ defaultMessage: 'Accepts HEX, RGB or HTML color names',
+ }),
+ getBackgroundColorLabel: () =>
+ i18n.translate('xpack.canvas.pageConfig.backgroundColorLabel', {
+ defaultMessage: 'Background',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.pageConfig.title', {
+ defaultMessage: 'Page settings',
+ }),
+ getTransitionLabel: () =>
+ i18n.translate('xpack.canvas.pageConfig.transitionLabel', {
+ defaultMessage: 'Transition',
+ description:
+ 'This refers to the transition effect, such as fade in or rotate, applied to a page in presentation mode.',
+ }),
+ getTransitionPreviewLabel: () =>
+ i18n.translate('xpack.canvas.pageConfig.transitionPreviewLabel', {
+ defaultMessage: 'Preview',
+ description: 'This is the label for a preview of the transition effect selected.',
+ }),
+};
export const PageConfig = ({
pageIndex,
diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx
index 06968d2e4be0a..9d1939db43fd5 100644
--- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx
+++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx
@@ -8,7 +8,9 @@
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { DragDropContext, Droppable, Draggable, DragDropContextProps } from 'react-beautiful-dnd';
+
// @ts-expect-error untyped dependency
import Style from 'style-it';
import { ConfirmModal } from '../confirm_modal';
@@ -16,11 +18,26 @@ import { RoutingLink } from '../routing';
import { WorkpadRoutingContext } from '../../routes/workpad';
import { PagePreview } from '../page_preview';
-import { ComponentStrings } from '../../../i18n';
import { CanvasPage } from '../../../types';
-const { PageManager: strings } = ComponentStrings;
-
+const strings = {
+ getAddPageTooltip: () =>
+ i18n.translate('xpack.canvas.pageManager.addPageTooltip', {
+ defaultMessage: 'Add a new page to this workpad',
+ }),
+ getConfirmRemoveTitle: () =>
+ i18n.translate('xpack.canvas.pageManager.confirmRemoveTitle', {
+ defaultMessage: 'Remove Page',
+ }),
+ getConfirmRemoveDescription: () =>
+ i18n.translate('xpack.canvas.pageManager.confirmRemoveDescription', {
+ defaultMessage: 'Are you sure you want to remove this page?',
+ }),
+ getConfirmRemoveButtonLabel: () =>
+ i18n.translate('xpack.canvas.pageManager.removeButtonLabel', {
+ defaultMessage: 'Remove',
+ }),
+};
export interface Props {
isWriteable: boolean;
onAddPage: () => void;
diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx
index b29ef1e7fd087..5246fcf822a72 100644
--- a/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx
+++ b/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx
@@ -8,10 +8,26 @@
import React, { FC, ReactEventHandler } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../i18n';
-
-const { PagePreviewPageControls: strings } = ComponentStrings;
+const strings = {
+ getClonePageAriaLabel: () =>
+ i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageAriaLabel', {
+ defaultMessage: 'Clone page',
+ }),
+ getClonePageTooltip: () =>
+ i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageTooltip', {
+ defaultMessage: 'Clone',
+ }),
+ getDeletePageAriaLabel: () =>
+ i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageAriaLabel', {
+ defaultMessage: 'Delete page',
+ }),
+ getDeletePageTooltip: () =>
+ i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageTooltip', {
+ defaultMessage: 'Delete',
+ }),
+};
interface Props {
pageId: string;
diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx
index 7ad7bcd8c49c2..dcc77b75f25c3 100644
--- a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx
+++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx
@@ -8,10 +8,20 @@
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { palettes, ColorPalette } from '../../../common/lib/palettes';
-import { ComponentStrings } from '../../../i18n';
-const { PalettePicker: strings } = ComponentStrings;
+const strings = {
+ getEmptyPaletteLabel: () =>
+ i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', {
+ defaultMessage: 'None',
+ }),
+ getNoPaletteFoundErrorTitle: () =>
+ i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', {
+ defaultMessage: 'Color palette not found',
+ }),
+};
interface RequiredProps {
id?: string;
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot
index bbf8b5dcca896..6cd18b83c3351 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot
@@ -94,16 +94,16 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = `
size="xxl"
/>
+
+ Add new elements
+
-
- Add new elements
-
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx
index 220ea193c902e..ad0a0053f55af 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx
@@ -8,9 +8,26 @@
import React, { FunctionComponent, MouseEvent } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n/components';
+import { i18n } from '@kbn/i18n';
-const { ElementControls: strings } = ComponentStrings;
+const strings = {
+ getDeleteAriaLabel: () =>
+ i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', {
+ defaultMessage: 'Delete element',
+ }),
+ getDeleteTooltip: () =>
+ i18n.translate('xpack.canvas.elementControls.deleteToolTip', {
+ defaultMessage: 'Delete',
+ }),
+ getEditAriaLabel: () =>
+ i18n.translate('xpack.canvas.elementControls.editAriaLabel', {
+ defaultMessage: 'Edit element',
+ }),
+ getEditTooltip: () =>
+ i18n.translate('xpack.canvas.elementControls.editToolTip', {
+ defaultMessage: 'Edit',
+ }),
+};
interface Props {
/**
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx
index bc0039245f432..ee14e89dc4b7d 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx
@@ -25,14 +25,59 @@ import {
EuiSpacer,
EuiButton,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { 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;
+const strings = {
+ 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',
+ }),
+ getFindElementPlaceholder: () =>
+ i18n.translate('xpack.canvas.savedElementsModal.findElementPlaceholder', {
+ defaultMessage: 'Find element',
+ }),
+ getModalTitle: () =>
+ i18n.translate('xpack.canvas.savedElementsModal.modalTitle', {
+ defaultMessage: 'My elements',
+ }),
+ getSavedElementsModalCloseButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', {
+ defaultMessage: 'Close',
+ }),
+};
export interface Props {
/**
diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx
index cc0ad5a728b17..e8f2c7a559f58 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx
+++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx
@@ -8,12 +8,28 @@
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { EuiTabbedContent } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
// @ts-expect-error unconverted component
import { Datasource } from '../../datasource';
// @ts-expect-error unconverted component
import { FunctionFormList } from '../../function_form_list';
import { PositionedElement } from '../../../../types';
-import { ComponentStrings } from '../../../../i18n';
+
+const strings = {
+ getDataTabLabel: () =>
+ i18n.translate('xpack.canvas.elementSettings.dataTabLabel', {
+ defaultMessage: 'Data',
+ description:
+ 'This tab contains the settings for the data (i.e. Elasticsearch query) used as ' +
+ 'the source for a Canvas element',
+ }),
+ getDisplayTabLabel: () =>
+ i18n.translate('xpack.canvas.elementSettings.displayTabLabel', {
+ defaultMessage: 'Display',
+ description: 'This tab contains the settings for how data is displayed in a Canvas element',
+ }),
+};
interface Props {
/**
@@ -22,8 +38,6 @@ interface Props {
element: PositionedElement;
}
-const { ElementSettings: strings } = ComponentStrings;
-
export const ElementSettings: FunctionComponent = ({ element }) => {
const tabs = [
{
diff --git a/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx
index e13cf338a2bdc..9d95a6978ff50 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx
+++ b/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx
@@ -7,9 +7,21 @@
import React, { FunctionComponent } from 'react';
import { EuiText } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n/components';
+import { i18n } from '@kbn/i18n';
-const { GroupSettings: strings } = ComponentStrings;
+const strings = {
+ getSaveGroupDescription: () =>
+ i18n.translate('xpack.canvas.groupSettings.saveGroupDescription', {
+ defaultMessage: 'Save this group as a new element to re-use it throughout your workpad.',
+ }),
+ getUngroupDescription: () =>
+ i18n.translate('xpack.canvas.groupSettings.ungroupDescription', {
+ defaultMessage: 'Ungroup ({uKey}) to edit individual element settings.',
+ values: {
+ uKey: 'U',
+ },
+ }),
+};
export const GroupSettings: FunctionComponent = () => (
diff --git a/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx
index f3bd11f603243..0d73e6397adcc 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx
+++ b/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx
@@ -7,9 +7,23 @@
import React, { FunctionComponent } from 'react';
import { EuiText } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n/components';
+import { i18n } from '@kbn/i18n';
-const { MultiElementSettings: strings } = ComponentStrings;
+const strings = {
+ getMultipleElementsActionsDescription: () =>
+ i18n.translate('xpack.canvas.groupSettings.multipleElementsActionsDescription', {
+ defaultMessage:
+ 'Deselect these elements to edit their individual settings, press ({gKey}) to group them, or save this selection as a new ' +
+ 'element to re-use it throughout your workpad.',
+ values: {
+ gKey: 'G',
+ },
+ }),
+ getMultipleElementsDescription: () =>
+ i18n.translate('xpack.canvas.groupSettings.multipleElementsDescription', {
+ defaultMessage: 'Multiple elements are currently selected.',
+ }),
+};
export const MultiElementSettings: FunctionComponent = () => (
diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js
index a284fc3278436..7292a98fa91ae 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js
+++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js
@@ -9,15 +9,39 @@ import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import { compose, branch, renderComponent } from 'recompose';
import { EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { getSelectedToplevelNodes, getSelectedElementId } from '../../state/selectors/workpad';
import { SidebarHeader } from '../sidebar_header';
-import { ComponentStrings } from '../../../i18n';
import { MultiElementSettings } from './multi_element_settings';
import { GroupSettings } from './group_settings';
import { GlobalConfig } from './global_config';
import { ElementSettings } from './element_settings';
-const { SidebarContent: strings } = ComponentStrings;
+const strings = {
+ getGroupedElementSidebarTitle: () =>
+ i18n.translate('xpack.canvas.sidebarContent.groupedElementSidebarTitle', {
+ defaultMessage: 'Grouped element',
+ description:
+ 'The title displayed when a grouped element is selected. "elements" refer to the different visualizations, images, ' +
+ 'text, etc that can be added in a Canvas workpad. These elements can be grouped into a larger "grouped element" ' +
+ 'that contains multiple individual elements.',
+ }),
+ getMultiElementSidebarTitle: () =>
+ i18n.translate('xpack.canvas.sidebarContent.multiElementSidebarTitle', {
+ defaultMessage: 'Multiple elements',
+ description:
+ 'The title displayed when multiple elements are selected. "elements" refer to the different visualizations, images, ' +
+ 'text, etc that can be added in a Canvas workpad.',
+ }),
+ getSingleElementSidebarTitle: () =>
+ i18n.translate('xpack.canvas.sidebarContent.singleElementSidebarTitle', {
+ defaultMessage: 'Selected element',
+ description:
+ 'The title displayed when a single element are selected. "element" refer to the different visualizations, images, ' +
+ 'text, etc that can be added in a Canvas workpad.',
+ }),
+};
const mapStateToProps = (state) => ({
selectedToplevelNodes: getSelectedToplevelNodes(state),
diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx
index d4f8c7642830d..4ba3a7f90f64b 100644
--- a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx
+++ b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx
@@ -8,11 +8,30 @@
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { ToolTipShortcut } from '../tool_tip_shortcut/';
-import { ComponentStrings } from '../../../i18n/components';
import { ShortcutStrings } from '../../../i18n/shortcuts';
-const { SidebarHeader: strings } = ComponentStrings;
+const strings = {
+ getBringForwardAriaLabel: () =>
+ i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', {
+ defaultMessage: 'Move element up one layer',
+ }),
+ getBringToFrontAriaLabel: () =>
+ i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', {
+ defaultMessage: 'Move element to top layer',
+ }),
+ getSendBackwardAriaLabel: () =>
+ i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', {
+ defaultMessage: 'Move element down one layer',
+ }),
+ getSendToBackAriaLabel: () =>
+ i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', {
+ defaultMessage: 'Move element to bottom layer',
+ }),
+};
+
const shortcutHelp = ShortcutStrings.getShortcutHelp();
interface Props {
diff --git a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx
index 51b9cf7d60262..8d4a1506ad8a2 100644
--- a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx
+++ b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx
@@ -8,13 +8,51 @@
import React, { FC, useState } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiSpacer, EuiButtonGroup } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { FontValue } from 'src/plugins/expressions';
-import { ComponentStrings } from '../../../i18n';
+
import { FontPicker } from '../font_picker';
import { ColorPickerPopover } from '../color_picker_popover';
import { fontSizes } from './font_sizes';
-const { TextStylePicker: strings } = ComponentStrings;
+const strings = {
+ getAlignCenterOption: () =>
+ i18n.translate('xpack.canvas.textStylePicker.alignCenterOption', {
+ defaultMessage: 'Align center',
+ }),
+ getAlignLeftOption: () =>
+ i18n.translate('xpack.canvas.textStylePicker.alignLeftOption', {
+ defaultMessage: 'Align left',
+ }),
+ getAlignRightOption: () =>
+ i18n.translate('xpack.canvas.textStylePicker.alignRightOption', {
+ defaultMessage: 'Align right',
+ }),
+ getAlignmentOptionsControlLegend: () =>
+ i18n.translate('xpack.canvas.textStylePicker.alignmentOptionsControl', {
+ defaultMessage: 'Alignment options',
+ }),
+ getFontColorLabel: () =>
+ i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', {
+ defaultMessage: 'Font Color',
+ }),
+ getStyleBoldOption: () =>
+ i18n.translate('xpack.canvas.textStylePicker.styleBoldOption', {
+ defaultMessage: 'Bold',
+ }),
+ getStyleItalicOption: () =>
+ i18n.translate('xpack.canvas.textStylePicker.styleItalicOption', {
+ defaultMessage: 'Italic',
+ }),
+ getStyleUnderlineOption: () =>
+ i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', {
+ defaultMessage: 'Underline',
+ }),
+ getStyleOptionsControlLegend: () =>
+ i18n.translate('xpack.canvas.textStylePicker.styleOptionsControl', {
+ defaultMessage: 'Style options',
+ }),
+};
export interface StyleProps {
family?: FontValue;
diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
index e4f297446701c..bd47bb52e0030 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
@@ -18,7 +18,6 @@ storiesOf('components/Toolbar', module)
isWriteable={true}
selectedPageNumber={1}
totalPages={1}
- workpadId={'abc'}
workpadName={'My Canvas Workpad'}
/>
))
@@ -28,7 +27,6 @@ storiesOf('components/Toolbar', module)
selectedElement={getDefaultElement()}
selectedPageNumber={1}
totalPages={1}
- workpadId={'abc'}
workpadName={'My Canvas Workpad'}
/>
));
diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
index baafbdafcc549..13cc4db7c6217 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
@@ -7,28 +7,40 @@
import React, { FC, useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiModal,
- EuiModalFooter,
- EuiButton,
-} from '@elastic/eui';
+import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-// @ts-expect-error untyped local
-import { WorkpadManager } from '../workpad_manager';
import { PageManager } from '../page_manager';
import { Expression } from '../expression';
import { Tray } from './tray';
import { CanvasElement } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
import { RoutingButtonIcon } from '../routing';
import { WorkpadRoutingContext } from '../../routes/workpad';
-const { Toolbar: strings } = ComponentStrings;
+const strings = {
+ getEditorButtonLabel: () =>
+ i18n.translate('xpack.canvas.toolbar.editorButtonLabel', {
+ defaultMessage: 'Expression editor',
+ }),
+ getNextPageAriaLabel: () =>
+ i18n.translate('xpack.canvas.toolbar.nextPageAriaLabel', {
+ defaultMessage: 'Next Page',
+ }),
+ getPageButtonLabel: (pageNum: number, totalPages: number) =>
+ i18n.translate('xpack.canvas.toolbar.pageButtonLabel', {
+ defaultMessage: 'Page {pageNum}{rest}',
+ values: {
+ pageNum,
+ rest: totalPages > 1 ? ` of ${totalPages}` : '',
+ },
+ }),
+ getPreviousPageAriaLabel: () =>
+ i18n.translate('xpack.canvas.toolbar.previousPageAriaLabel', {
+ defaultMessage: 'Previous Page',
+ }),
+};
type TrayType = 'pageManager' | 'expression';
@@ -37,7 +49,6 @@ export interface Props {
selectedElement?: CanvasElement;
selectedPageNumber: number;
totalPages: number;
- workpadId: string;
workpadName: string;
}
@@ -46,11 +57,9 @@ export const Toolbar: FC
= ({
selectedElement,
selectedPageNumber,
totalPages,
- workpadId,
workpadName,
}) => {
const [activeTray, setActiveTray] = useState(null);
- const [showWorkpadManager, setShowWorkpadManager] = useState(false);
const { getUrl, previousPage } = useContext(WorkpadRoutingContext);
// While the tray doesn't get activated if the workpad isn't writeable,
@@ -75,20 +84,6 @@ export const Toolbar: FC = ({
}
};
- const closeWorkpadManager = () => setShowWorkpadManager(false);
- const openWorkpadManager = () => setShowWorkpadManager(true);
-
- const workpadManager = (
-
-
-
-
- {strings.getWorkpadManagerCloseButtonLabel()}
-
-
-
- );
-
const trays = {
pageManager: ,
expression: !elementIsSelected ? null : setActiveTray(null)} />,
@@ -99,12 +94,6 @@ export const Toolbar: FC = ({
{activeTray !== null && setActiveTray(null)}>{trays[activeTray]} }
-
- openWorkpadManager()}>
- {workpadName}
-
-
-
= ({
)}
- {showWorkpadManager && workpadManager}
);
};
@@ -153,6 +141,5 @@ Toolbar.propTypes = {
selectedElement: PropTypes.object,
selectedPageNumber: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired,
- workpadId: PropTypes.string.isRequired,
workpadName: PropTypes.string.isRequired,
};
diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx
index 0230eb86e121a..bc6eb455bb9b6 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx
@@ -8,9 +8,14 @@
import React, { ReactNode, MouseEventHandler } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../../i18n';
-const { ToolbarTray: strings } = ComponentStrings;
+const strings = {
+ getCloseTrayAriaLabel: () =>
+ i18n.translate('xpack.canvas.toolbarTray.closeTrayAriaLabel', {
+ defaultMessage: 'Close tray',
+ }),
+};
interface Props {
children: ReactNode;
diff --git a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx
index 69b3306d85ea5..f6ba2d7e28825 100644
--- a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx
@@ -15,10 +15,29 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { CanvasVariable } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-const { VarConfigDeleteVar: strings } = ComponentStrings;
+const strings = {
+ getCancelButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfigDeleteVar.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+ getDeleteButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfigDeleteVar.deleteButtonLabel', {
+ defaultMessage: 'Delete variable',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.varConfigDeleteVar.titleLabel', {
+ defaultMessage: 'Delete variable?',
+ }),
+ getWarningDescription: () =>
+ i18n.translate('xpack.canvas.varConfigDeleteVar.warningDescription', {
+ defaultMessage:
+ 'Deleting this variable may adversely affect the workpad. Are you sure you wish to continue?',
+ }),
+};
import './var_panel.scss';
diff --git a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx
index 64ec8af291448..35f9e67745aec 100644
--- a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx
@@ -20,12 +20,61 @@ import {
EuiSpacer,
EuiCallOut,
} from '@elastic/eui';
-import { CanvasVariable } from '../../../types';
+import { i18n } from '@kbn/i18n';
+import { CanvasVariable } from '../../../types';
import { VarValueField } from './var_value_field';
-import { ComponentStrings } from '../../../i18n';
-const { VarConfigEditVar: strings } = ComponentStrings;
+const strings = {
+ getAddTitle: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.addTitleLabel', {
+ defaultMessage: 'Add variable',
+ }),
+ getCancelButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+ getDuplicateNameError: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.duplicateNameError', {
+ defaultMessage: 'Variable name already in use',
+ }),
+ getEditTitle: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.editTitleLabel', {
+ defaultMessage: 'Edit variable',
+ }),
+ getEditWarning: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.editWarning', {
+ defaultMessage: 'Editing a variable in use may adversely affect your workpad',
+ }),
+ getNameFieldLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.nameFieldLabel', {
+ defaultMessage: 'Name',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.saveButtonLabel', {
+ defaultMessage: 'Save changes',
+ }),
+ getTypeBooleanLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.typeBooleanLabel', {
+ defaultMessage: 'Boolean',
+ }),
+ getTypeFieldLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.typeFieldLabel', {
+ defaultMessage: 'Type',
+ }),
+ getTypeNumberLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.typeNumberLabel', {
+ defaultMessage: 'Number',
+ }),
+ getTypeStringLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.typeStringLabel', {
+ defaultMessage: 'String',
+ }),
+ getValueFieldLabel: () =>
+ i18n.translate('xpack.canvas.varConfigEditVar.valueFieldLabel', {
+ defaultMessage: 'Value',
+ }),
+};
import './edit_var.scss';
import './var_panel.scss';
diff --git a/x-pack/plugins/canvas/public/components/var_config/index.tsx b/x-pack/plugins/canvas/public/components/var_config/index.tsx
index 3f072e2f95140..db2a84e93a5dc 100644
--- a/x-pack/plugins/canvas/public/components/var_config/index.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/index.tsx
@@ -7,12 +7,22 @@
import React, { FC } from 'react';
import copy from 'copy-to-clipboard';
+import { i18n } from '@kbn/i18n';
+
import { VarConfig as ChildComponent } from './var_config';
import { useNotifyService } from '../../services';
-import { ComponentStrings } from '../../../i18n';
import { CanvasVariable } from '../../../types';
-const { VarConfig: strings } = ComponentStrings;
+const strings = {
+ getCopyNotificationDescription: () =>
+ i18n.translate('xpack.canvas.varConfig.copyNotificationDescription', {
+ defaultMessage: 'Variable syntax copied to clipboard',
+ }),
+ getDeleteNotificationDescription: () =>
+ i18n.translate('xpack.canvas.varConfig.deleteNotificationDescription', {
+ defaultMessage: 'Variable successfully deleted',
+ }),
+};
interface Props {
variables: CanvasVariable[];
diff --git a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx
index 0fe506715d07d..dc8898e2132e7 100644
--- a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx
@@ -18,17 +18,15 @@ import {
EuiSpacer,
EuiButton,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { CanvasVariable } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
import { EditVar } from './edit_var';
import { DeleteVar } from './delete_var';
import './var_config.scss';
-const { VarConfig: strings } = ComponentStrings;
-
enum PanelMode {
List,
Edit,
@@ -49,6 +47,58 @@ interface Props {
onEditVar: (oldVar: CanvasVariable, newVar: CanvasVariable) => void;
}
+const strings = {
+ getAddButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.addButtonLabel', {
+ defaultMessage: 'Add a variable',
+ }),
+ getAddTooltipLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.addTooltipLabel', {
+ defaultMessage: 'Add a variable',
+ }),
+ getCopyActionButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.copyActionButtonLabel', {
+ defaultMessage: 'Copy snippet',
+ }),
+ getCopyActionTooltipLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.copyActionTooltipLabel', {
+ defaultMessage: 'Copy variable syntax to clipboard',
+ }),
+ getDeleteActionButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.deleteActionButtonLabel', {
+ defaultMessage: 'Delete variable',
+ }),
+ getEditActionButtonLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.editActionButtonLabel', {
+ defaultMessage: 'Edit variable',
+ }),
+ getEmptyDescription: () =>
+ i18n.translate('xpack.canvas.varConfig.emptyDescription', {
+ defaultMessage:
+ 'This workpad has no variables currently. You may add variables to store and edit common values. These variables can then be used in elements or within the expression editor.',
+ }),
+ getTableNameLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.tableNameLabel', {
+ defaultMessage: 'Name',
+ }),
+ getTableTypeLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.tableTypeLabel', {
+ defaultMessage: 'Type',
+ }),
+ getTableValueLabel: () =>
+ i18n.translate('xpack.canvas.varConfig.tableValueLabel', {
+ defaultMessage: 'Value',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.varConfig.titleLabel', {
+ defaultMessage: 'Variables',
+ }),
+ getTitleTooltip: () =>
+ i18n.translate('xpack.canvas.varConfig.titleTooltip', {
+ defaultMessage: 'Add variables to store and edit common values',
+ }),
+};
+
export const VarConfig: FC
= ({
variables,
onCopyVar,
diff --git a/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx b/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx
index c89164dc6efd4..1232ba3977d70 100644
--- a/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx
@@ -8,11 +8,24 @@
import React, { FC } from 'react';
import { EuiFieldText, EuiFieldNumber, EuiButtonGroup } from '@elastic/eui';
import { htmlIdGenerator } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { CanvasVariable } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-const { VarConfigVarValueField: strings } = ComponentStrings;
+const strings = {
+ getBooleanOptionsLegend: () =>
+ i18n.translate('xpack.canvas.varConfigVarValueField.booleanOptionsLegend', {
+ defaultMessage: 'Boolean value',
+ }),
+ getFalseOption: () =>
+ i18n.translate('xpack.canvas.varConfigVarValueField.falseOption', {
+ defaultMessage: 'False',
+ }),
+ getTrueOption: () =>
+ i18n.translate('xpack.canvas.varConfigVarValueField.trueOption', {
+ defaultMessage: 'True',
+ }),
+};
interface Props {
type: CanvasVariable['type'];
diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx
index cc6271e376c07..0561ac005519b 100644
--- a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx
@@ -6,10 +6,15 @@
*/
import React from 'react';
+import { i18n } from '@kbn/i18n';
import { ColorPickerPopover, Props } from '../color_picker_popover';
-import { ComponentStrings } from '../../../i18n';
-const { WorkpadConfig: strings } = ComponentStrings;
+const strings = {
+ getBackgroundColorLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', {
+ defaultMessage: 'Background color',
+ }),
+};
export const WorkpadColorPicker = (props: Props) => {
return (
diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx
index 2776280d17b32..18e3f2dac9777 100644
--- a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx
@@ -22,14 +22,70 @@ import {
EuiAccordion,
EuiButton,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { VarConfig } from '../var_config';
-
import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants';
import { CanvasVariable } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-const { WorkpadConfig: strings } = ComponentStrings;
+const strings = {
+ getApplyStylesheetButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.applyStylesheetButtonLabel', {
+ defaultMessage: `Apply stylesheet`,
+ description: '"stylesheet" refers to the collection of CSS style rules entered by the user.',
+ }),
+ getFlipDimensionAriaLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.swapDimensionsAriaLabel', {
+ defaultMessage: `Swap the page's width and height`,
+ }),
+ getFlipDimensionTooltip: () =>
+ i18n.translate('xpack.canvas.workpadConfig.swapDimensionsTooltip', {
+ defaultMessage: 'Swap the width and height',
+ }),
+ getGlobalCSSLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.globalCSSLabel', {
+ defaultMessage: `Global CSS overrides`,
+ }),
+ getGlobalCSSTooltip: () =>
+ i18n.translate('xpack.canvas.workpadConfig.globalCSSTooltip', {
+ defaultMessage: `Apply styles to all pages in this workpad`,
+ }),
+ getNameLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.nameLabel', {
+ defaultMessage: 'Name',
+ }),
+ getPageHeightLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.heightLabel', {
+ defaultMessage: 'Height',
+ }),
+ getPageSizeBadgeAriaLabel: (sizeName: string) =>
+ i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeAriaLabel', {
+ defaultMessage: `Preset page size: {sizeName}`,
+ values: {
+ sizeName,
+ },
+ }),
+ getPageSizeBadgeOnClickAriaLabel: (sizeName: string) =>
+ i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeOnClickAriaLabel', {
+ defaultMessage: `Set page size to {sizeName}`,
+ values: {
+ sizeName,
+ },
+ }),
+ getPageWidthLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.widthLabel', {
+ defaultMessage: 'Width',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.workpadConfig.title', {
+ defaultMessage: 'Workpad settings',
+ }),
+ getUSLetterButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', {
+ defaultMessage: 'US Letter',
+ description: 'This is referring to the dimensions of U.S. standard letter paper.',
+ }),
+};
export interface Props {
size: {
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx
index cb66eceac97c3..c78bdb2a78821 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx
@@ -8,7 +8,8 @@
import React, { Fragment, FunctionComponent, useState } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui';
-import { ComponentStrings } from '../../../../i18n/components';
+import { i18n } from '@kbn/i18n';
+
import { ShortcutStrings } from '../../../../i18n/shortcuts';
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
import { Popover, ClosePopoverFn } from '../../popover';
@@ -16,8 +17,95 @@ import { CustomElementModal } from '../../custom_element_modal';
import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib/constants';
import { PositionedElement } from '../../../../types';
-const { WorkpadHeaderEditMenu: strings } = ComponentStrings;
const shortcutHelp = ShortcutStrings.getShortcutHelp();
+const strings = {
+ getAlignmentMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel', {
+ defaultMessage: 'Alignment',
+ description:
+ 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' +
+ 'alignment options of the selected elements',
+ }),
+ getBottomAlignMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel', {
+ defaultMessage: 'Bottom',
+ }),
+ getCenterAlignMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel', {
+ defaultMessage: 'Center',
+ description: 'This refers to alignment centered horizontally.',
+ }),
+ getCreateElementModalTitle: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.createElementModalTitle', {
+ defaultMessage: 'Create new element',
+ }),
+ getDistributionMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel', {
+ defaultMessage: 'Distribution',
+ description:
+ 'This refers to the options to evenly spacing the selected elements horizontall or vertically.',
+ }),
+ getEditMenuButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuButtonLabel', {
+ defaultMessage: 'Edit',
+ }),
+ getEditMenuLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuLabel', {
+ defaultMessage: 'Edit options',
+ }),
+ getGroupMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel', {
+ defaultMessage: 'Group',
+ description: 'This refers to grouping multiple selected elements.',
+ }),
+ getHorizontalDistributionMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel', {
+ defaultMessage: 'Horizontal',
+ }),
+ getLeftAlignMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel', {
+ defaultMessage: 'Left',
+ }),
+ getMiddleAlignMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel', {
+ defaultMessage: 'Middle',
+ description: 'This refers to alignment centered vertically.',
+ }),
+ getOrderMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel', {
+ defaultMessage: 'Order',
+ description: 'Refers to the order of the elements displayed on the page from front to back',
+ }),
+ getRedoMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.redoMenuItemLabel', {
+ defaultMessage: 'Redo',
+ }),
+ getRightAlignMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel', {
+ defaultMessage: 'Right',
+ }),
+ getSaveElementMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel', {
+ defaultMessage: 'Save as new element',
+ }),
+ getTopAlignMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel', {
+ defaultMessage: 'Top',
+ }),
+ getUndoMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.undoMenuItemLabel', {
+ defaultMessage: 'Undo',
+ }),
+ getUngroupMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel', {
+ defaultMessage: 'Ungroup',
+ description: 'This refers to ungrouping a grouped element',
+ }),
+ getVerticalDistributionMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel', {
+ defaultMessage: 'Vertical',
+ }),
+};
export interface Props {
/**
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx
index 19414f7c8d964..e1d69163e0761 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx
@@ -14,8 +14,9 @@ import {
EuiIcon,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
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';
@@ -31,7 +32,56 @@ interface ElementTypeMeta {
[key: string]: { name: string; icon: string };
}
-export const { WorkpadHeaderElementMenu: strings } = ComponentStrings;
+const strings = {
+ 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 Kibana',
+ }),
+ 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',
+ }),
+};
// label and icon for the context menu item for each element type
const elementTypeMeta: ElementTypeMeta = {
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx
index eea59e6aa49f3..fde21c7c85c37 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx
@@ -7,15 +7,21 @@
import React, { useState } from 'react';
import { EuiButtonEmpty, EuiNotificationBadge } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import {
LazyLabsFlyout,
withSuspense,
} from '../../../../../../../src/plugins/presentation_util/public';
-import { ComponentStrings } from '../../../../i18n';
import { useLabsService } from '../../../services';
-const { LabsControl: strings } = ComponentStrings;
+
+const strings = {
+ getLabsButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel', {
+ defaultMessage: 'Labs',
+ }),
+};
const Flyout = withSuspense(LazyLabsFlyout, null);
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx
index dd9ddc2707ba6..7b1df158087b4 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx
@@ -8,10 +8,20 @@
import React, { MouseEventHandler } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { ToolTipShortcut } from '../../tool_tip_shortcut';
-import { ComponentStrings } from '../../../../i18n';
-const { WorkpadHeaderRefreshControlSettings: strings } = ComponentStrings;
+const strings = {
+ getRefreshAriaLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel', {
+ defaultMessage: 'Refresh Elements',
+ }),
+ getRefreshTooltip: () =>
+ i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip', {
+ defaultMessage: 'Refresh data',
+ }),
+};
export interface Props {
doRefresh: MouseEventHandler;
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot
index 010037bee4a0f..75ee0fcae78f3 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot
@@ -37,7 +37,7 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = `
>
Remove borders and footer logo
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx
index 7c90a6fb045b7..5da009e050a27 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx
@@ -21,16 +21,46 @@ import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { ComponentStrings } from '../../../../../i18n/components';
import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants';
import { OnCloseFn } from '../share_menu.component';
import { WorkpadStep } from './workpad_step';
import { RuntimeStep } from './runtime_step';
import { SnippetsStep } from './snippets_step';
-const { ShareWebsiteFlyout: strings } = ComponentStrings;
+const strings = {
+ getRuntimeStepTitle: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', {
+ defaultMessage: 'Download runtime',
+ }),
+ getSnippentsStepTitle: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.addSnippetsTitle', {
+ defaultMessage: 'Add snippets to website',
+ }),
+ getStepsDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.description', {
+ defaultMessage:
+ 'Follow these steps to share a static version of this workpad on an external website. It will be a visual snapshot of the current workpad, and will not have access to live data.',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.flyoutTitle', {
+ defaultMessage: 'Share on a website',
+ }),
+ getUnsupportedRendererWarning: () =>
+ 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: {
+ CANVAS,
+ },
+ }),
+ getWorkpadStepTitle: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadWorkpadTitle', {
+ defaultMessage: 'Download workpad',
+ }),
+};
export type OnDownloadFn = (type: 'share' | 'shareRuntime' | 'shareZip') => void;
export type OnCopyFn = () => void;
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts
index 05d0070a5ea69..65c9d6598578d 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts
@@ -7,6 +7,8 @@
import { connect } from 'react-redux';
import { compose, withProps } from 'recompose';
+import { i18n } from '@kbn/i18n';
+
import {
getWorkpad,
getRenderedWorkpad,
@@ -24,14 +26,35 @@ 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.component';
+import { ZIP } from '../../../../../i18n/constants';
import { WithKibanaProps } from '../../../../index';
export { OnDownloadFn, OnCopyFn } from './flyout.component';
-const { WorkpadHeaderShareMenu: strings } = ComponentStrings;
+const strings = {
+ getCopyShareConfigMessage: () =>
+ i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', {
+ defaultMessage: 'Copied share markup to clipboard',
+ }),
+ getShareableZipErrorTitle: (workpadName: string) =>
+ 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: {
+ ZIP,
+ workpadName,
+ },
+ }),
+ getUnknownExportErrorMessage: (type: string) =>
+ i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
+ defaultMessage: 'Unknown export type: {type}',
+ values: {
+ type,
+ },
+ }),
+};
const getUnsupportedRenderers = (state: State) => {
const renderers: string[] = [];
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx
index c686c403a9a45..8b2fe1a1c0394 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx
@@ -7,12 +7,26 @@
import React, { FC } from 'react';
import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../../../i18n/components';
+import { CANVAS } from '../../../../../i18n/constants';
import { OnDownloadFn } from './flyout';
-const { ShareWebsiteRuntimeStep: strings } = ComponentStrings;
+const strings = {
+ getDownloadLabel: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.downloadLabel', {
+ defaultMessage: 'Download runtime',
+ }),
+ getStepDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.description', {
+ defaultMessage:
+ 'In order to render a Shareable Workpad, you also need to include the {CANVAS} Shareable Workpad Runtime. You can skip this step if the runtime is already included on your website.',
+ values: {
+ CANVAS,
+ },
+ }),
+};
export const RuntimeStep: FC<{ onDownload: OnDownloadFn }> = ({ onDownload }) => (
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx
index bc9f123c623f6..1bac3068e7dbb 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx
@@ -16,13 +16,91 @@ import {
EuiDescriptionListDescription,
EuiHorizontalRule,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../../../i18n/components';
+import { CANVAS, URL, JSON } from '../../../../../i18n/constants';
import { Clipboard } from '../../../clipboard';
import { OnCopyFn } from './flyout';
-const { ShareWebsiteSnippetsStep: strings } = ComponentStrings;
+const strings = {
+ getAutoplayParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.autoplayParameterDescription', {
+ defaultMessage: 'Should the runtime automatically move through the pages of the workpad?',
+ }),
+ getCallRuntimeLabel: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.callRuntimeLabel', {
+ defaultMessage: 'Call Runtime',
+ }),
+ getHeightParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.heightParameterDescription', {
+ defaultMessage: 'The height of the Workpad. Defaults to the Workpad height.',
+ }),
+ getIncludeRuntimeLabel: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.includeRuntimeLabel', {
+ defaultMessage: 'Include Runtime',
+ }),
+ getIntervalParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.intervalParameterDescription', {
+ defaultMessage:
+ 'The interval upon which the pages will advance in time format, (e.g. {twoSeconds}, {oneMinute})',
+ values: {
+ twoSeconds: '2s',
+ oneMinute: '1m',
+ },
+ }),
+ getPageParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.pageParameterDescription', {
+ defaultMessage: 'The page to display. Defaults to the page specified by the Workpad.',
+ }),
+ getParametersDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersDescription', {
+ defaultMessage: 'There are a number of inline parameters to configure the Shareable Workpad.',
+ }),
+ getParametersTitle: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersLabel', {
+ defaultMessage: 'Parameters',
+ }),
+ getPlaceholderLabel: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.placeholderLabel', {
+ defaultMessage: 'Placeholder',
+ }),
+ getRequiredLabel: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.requiredLabel', {
+ defaultMessage: 'required',
+ }),
+ getShareableParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.shareableParameterDescription', {
+ defaultMessage: 'The type of shareable. In this case, a {CANVAS} Workpad.',
+ values: {
+ CANVAS,
+ },
+ }),
+ getSnippetsStepDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.description', {
+ defaultMessage:
+ 'The Workpad is placed within the {HTML} of the site by using an {HTML} placeholder. Parameters for the runtime are included inline. See the full list of parameters below. You can include more than one workpad on the page.',
+ values: {
+ HTML,
+ },
+ }),
+ getToolbarParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.toolbarParameterDescription', {
+ defaultMessage: 'Should the toolbar be hidden?',
+ }),
+ getUrlParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.urlParameterDescription', {
+ defaultMessage: 'The {URL} of the Shareable Workpad {JSON} file.',
+ values: {
+ URL,
+ JSON,
+ },
+ }),
+ getWidthParameterDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.widthParameterDescription', {
+ defaultMessage: 'The width of the Workpad. Defaults to the Workpad width.',
+ }),
+};
const HTML = `
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx
index c5a6a4478c765..3ab358d0fe324 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx
@@ -7,12 +7,26 @@
import React, { FC } from 'react';
import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../../../i18n/components';
+import { JSON } from '../../../../../i18n/constants';
import { OnDownloadFn } from './flyout';
-const { ShareWebsiteWorkpadStep: strings } = ComponentStrings;
+const strings = {
+ getDownloadLabel: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.downloadLabel', {
+ defaultMessage: 'Download workpad',
+ }),
+ getStepDescription: () =>
+ i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.description', {
+ defaultMessage:
+ 'The workpad will be exported as a single {JSON} file for sharing in another site.',
+ values: {
+ JSON,
+ },
+ }),
+};
export const WorkpadStep: FC<{ onDownload: OnDownloadFn }> = ({ onDownload }) => (
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx
index d4cb4d0736bb1..5ccc09bf3586b 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx
@@ -5,18 +5,47 @@
* 2.0.
*/
+import React, { FunctionComponent, useState } from 'react';
+import PropTypes from 'prop-types';
import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { IBasePath } from 'kibana/public';
-import PropTypes from 'prop-types';
-import React, { FunctionComponent, useState } from 'react';
+
import { ReportingStart } from '../../../../../reporting/public';
-import { ComponentStrings } from '../../../../i18n/components';
+import { PDF, JSON } from '../../../../i18n/constants';
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
import { ClosePopoverFn, Popover } from '../../popover';
import { ShareWebsiteFlyout } from './flyout';
import { CanvasWorkpadSharingData, getPdfJobParams } from './utils';
-const { WorkpadHeaderShareMenu: strings } = ComponentStrings;
+const strings = {
+ getShareDownloadJSONTitle: () =>
+ i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle', {
+ defaultMessage: 'Download as {JSON}',
+ values: {
+ JSON,
+ },
+ }),
+ getShareDownloadPDFTitle: () =>
+ 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.workpadHeaderShareMenu.shareWebsiteTitle', {
+ defaultMessage: 'Share on a website',
+ }),
+ getShareWorkpadMessage: () =>
+ i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage', {
+ defaultMessage: 'Share this workpad',
+ }),
+};
type CopyTypes = 'pdf' | 'reportingConfig';
type ExportTypes = 'pdf' | 'json';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts
index fc4906817cf6f..ef13655b66aca 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts
@@ -7,14 +7,23 @@
import { connect } from 'react-redux';
import { compose, withProps } from 'recompose';
-import { ComponentStrings } from '../../../../i18n';
+import { i18n } from '@kbn/i18n';
+
import { CanvasWorkpad, State } from '../../../../types';
import { downloadWorkpad } from '../../../lib/download_workpad';
import { withServices, WithServicesProps } from '../../../services';
import { getPages, getWorkpad } from '../../../state/selectors/workpad';
import { Props as ComponentProps, ShareMenu as Component } from './share_menu.component';
-const { WorkpadHeaderShareMenu: strings } = ComponentStrings;
+const strings = {
+ getUnknownExportErrorMessage: (type: string) =>
+ i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
+ defaultMessage: 'Unknown export type: {type}',
+ values: {
+ type,
+ },
+ }),
+};
const mapStateToProps = (state: State) => ({
workpad: getWorkpad(state),
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx
index 1508f8683b8c1..6815ef351e0b8 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx
@@ -22,14 +22,34 @@ import {
EuiToolTip,
htmlIdGenerator,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { timeDuration } from '../../../lib/time_duration';
+import { UnitStrings } from '../../../../i18n';
import { CustomInterval } from './custom_interval';
-import { ComponentStrings, UnitStrings } from '../../../../i18n';
-const { WorkpadHeaderAutoRefreshControls: strings } = ComponentStrings;
const { time: timeStrings } = UnitStrings;
const { getSecondsText, getMinutesText, getHoursText } = timeStrings;
+const strings = {
+ getDisableTooltip: () =>
+ i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.disableTooltip', {
+ defaultMessage: 'Disable auto-refresh',
+ }),
+ getIntervalFormLabelText: () =>
+ i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel', {
+ defaultMessage: 'Change auto-refresh interval',
+ }),
+ getRefreshListDurationManualText: () =>
+ i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText', {
+ defaultMessage: 'Manually',
+ }),
+ getRefreshListTitle: () =>
+ i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle', {
+ defaultMessage: 'Refresh elements',
+ }),
+};
+
interface Props {
refreshInterval: number;
setRefresh: (interval: number) => void;
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx
index d4d28d19131f0..284749340e440 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx
@@ -8,12 +8,31 @@
import React, { useState, ChangeEvent } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiButton, EuiFieldText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { ButtonSize } from '@elastic/eui/src/components/button/button';
import { FlexGroupGutterSize } from '@elastic/eui/src/components/flex/flex_group';
import { getTimeInterval } from '../../../lib/time_interval';
-import { ComponentStrings } from '../../../../i18n';
-const { WorkpadHeaderCustomInterval: strings } = ComponentStrings;
+const strings = {
+ getButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel', {
+ defaultMessage: 'Set',
+ }),
+ getFormDescription: () =>
+ i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formDescription', {
+ defaultMessage:
+ 'Use shorthand notation, like {secondsExample}, {minutesExample}, or {hoursExample}',
+ values: {
+ secondsExample: '30s',
+ minutesExample: '10m',
+ hoursExample: '1h',
+ },
+ }),
+ getFormLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formLabel', {
+ defaultMessage: 'Set a custom interval',
+ }),
+};
interface Props {
gutterSize: FlexGroupGutterSize;
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx
index 55373d7a3515c..b8ed80c870f28 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx
@@ -22,14 +22,34 @@ import {
EuiFlexGroup,
htmlIdGenerator,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { timeDuration } from '../../../lib/time_duration';
+import { UnitStrings } from '../../../../i18n';
import { CustomInterval } from './custom_interval';
-import { ComponentStrings, UnitStrings } from '../../../../i18n';
-const { WorkpadHeaderKioskControls: strings } = ComponentStrings;
const { time: timeStrings } = UnitStrings;
const { getSecondsText, getMinutesText } = timeStrings;
+const strings = {
+ getCycleFormLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleFormLabel', {
+ defaultMessage: 'Change cycling interval',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.workpadHeaderKioskControl.controlTitle', {
+ defaultMessage: 'Cycle fullscreen pages',
+ }),
+ getAutoplayListDurationManualText: () =>
+ i18n.translate('xpack.canvas.workpadHeaderKioskControl.autoplayListDurationManual', {
+ defaultMessage: 'Manually',
+ }),
+ getDisableTooltip: () =>
+ i18n.translate('xpack.canvas.workpadHeaderKioskControl.disableTooltip', {
+ defaultMessage: 'Disable auto-play',
+ }),
+};
+
interface Props {
autoplayInterval: number;
onSetInterval: (interval: number) => void;
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx
index 8fb24c1f3c62e..168ddc690c4d4 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx
@@ -13,18 +13,80 @@ import {
EuiIcon,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import {
MAX_ZOOM_LEVEL,
MIN_ZOOM_LEVEL,
CONTEXT_MENU_TOP_BORDER_CLASSNAME,
} from '../../../../common/lib/constants';
-import { ComponentStrings } from '../../../../i18n/components';
+
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
import { Popover, ClosePopoverFn } from '../../popover';
import { AutoRefreshControls } from './auto_refresh_controls';
import { KioskControls } from './kiosk_controls';
-const { WorkpadHeaderViewMenu: strings } = ComponentStrings;
+const strings = {
+ getAutoplaySettingsMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel', {
+ defaultMessage: 'Autoplay settings',
+ }),
+ 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',
+ }),
+ getRefreshSettingsMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshSettingsMenuItemLabel', {
+ defaultMessage: 'Auto refresh settings',
+ }),
+ 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',
+ }),
+ getZoomFitToWindowText: () =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText', {
+ defaultMessage: 'Fit to window',
+ }),
+ getZoomInText: () =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomInText', {
+ defaultMessage: 'Zoom in',
+ }),
+ getZoomMenuItemLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel', {
+ defaultMessage: 'Zoom',
+ }),
+ getZoomOutText: () =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomOutText', {
+ defaultMessage: 'Zoom out',
+ }),
+ getZoomPercentage: (scale: number) =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomResetText', {
+ defaultMessage: '{scalePercentage}%',
+ values: {
+ scalePercentage: scale * 100,
+ },
+ }),
+ getZoomResetText: () =>
+ i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue', {
+ defaultMessage: 'Reset',
+ }),
+};
const QUICK_ZOOM_LEVELS = [0.5, 1, 2];
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx
index 415d3ddf46709..5320a65a90408 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx
@@ -10,7 +10,8 @@ import PropTypes from 'prop-types';
// @ts-expect-error no @types definition
import { Shortcuts } from 'react-shortcuts';
import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
+
import { ToolTipShortcut } from '../tool_tip_shortcut/';
import { RefreshControl } from './refresh_control';
// @ts-expect-error untyped local
@@ -22,7 +23,28 @@ import { ViewMenu } from './view_menu';
import { LabsControl } from './labs_control';
import { CommitFn } from '../../../types';
-const { WorkpadHeader: strings } = ComponentStrings;
+const strings = {
+ getFullScreenButtonAriaLabel: () =>
+ i18n.translate('xpack.canvas.workpadHeader.fullscreenButtonAriaLabel', {
+ defaultMessage: 'View fullscreen',
+ }),
+ getFullScreenTooltip: () =>
+ i18n.translate('xpack.canvas.workpadHeader.fullscreenTooltip', {
+ defaultMessage: 'Enter fullscreen mode',
+ }),
+ getHideEditControlTooltip: () =>
+ i18n.translate('xpack.canvas.workpadHeader.hideEditControlTooltip', {
+ defaultMessage: 'Hide editing controls',
+ }),
+ getNoWritePermissionTooltipText: () =>
+ i18n.translate('xpack.canvas.workpadHeader.noWritePermissionTooltip', {
+ defaultMessage: "You don't have permission to edit this workpad",
+ }),
+ getShowEditControlTooltip: () =>
+ i18n.translate('xpack.canvas.workpadHeader.showEditControlTooltip', {
+ defaultMessage: 'Show editing controls',
+ }),
+};
export interface Props {
isWriteable: boolean;
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
deleted file mode 100644
index 2afd5fe70abe1..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { FC, useState, useCallback } from 'react';
-import { useHistory } from 'react-router-dom';
-import { useSelector } from 'react-redux';
-import moment from 'moment';
-// @ts-expect-error
-import { getDefaultWorkpad } from '../../state/defaults';
-import { canUserWrite as canUserWriteSelector } 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 { State, CanvasWorkpad } from '../../../types';
-import { useNotifyService, useWorkpadService, usePlatformService } from '../../services';
-// @ts-expect-error
-import { WorkpadLoader as Component } from './workpad_loader';
-
-const { WorkpadLoader: strings } = ComponentStrings;
-const { WorkpadLoader: errors } = ErrorStrings;
-
-type WorkpadStatePromise = ReturnType['find']>;
-type WorkpadState = WorkpadStatePromise extends PromiseLike ? U : never;
-
-export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => {
- const fromState = useSelector((state: State) => ({
- workpadId: getWorkpad(state).id,
- canUserWrite: canUserWriteSelector(state),
- }));
-
- const [workpadsState, setWorkpadsState] = useState(null);
- const workpadService = useWorkpadService();
- const notifyService = useNotifyService();
- const platformService = usePlatformService();
- const history = useHistory();
-
- const createWorkpad = useCallback(
- async (_workpad: CanvasWorkpad | null | undefined) => {
- const workpad = _workpad || getDefaultWorkpad();
- if (workpad != null) {
- try {
- await workpadService.create(workpad);
- history.push(`/workpad/${workpad.id}/page/1`);
- } catch (err) {
- notifyService.error(err, {
- title: errors.getUploadFailureErrorMessage(),
- });
- }
- return;
- }
- },
- [workpadService, notifyService, history]
- );
-
- const findWorkpads = useCallback(
- async (text) => {
- try {
- const fetchedWorkpads = await workpadService.find(text);
- setWorkpadsState(fetchedWorkpads);
- } catch (err) {
- notifyService.error(err, { title: errors.getFindFailureErrorMessage() });
- }
- },
- [notifyService, workpadService]
- );
-
- const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []);
-
- const cloneWorkpad = useCallback(
- async (workpadId: string) => {
- try {
- const workpad = await workpadService.get(workpadId);
- workpad.name = strings.getClonedWorkpadName(workpad.name);
- workpad.id = getId('workpad');
- await workpadService.create(workpad);
- history.push(`/workpad/${workpad.id}/page/1`);
- } catch (err) {
- notifyService.error(err, { title: errors.getCloneFailureErrorMessage() });
- }
- },
- [notifyService, workpadService, history]
- );
-
- const removeWorkpads = useCallback(
- (workpadIds: string[]) => {
- if (workpadsState === null) {
- return;
- }
-
- const removedWorkpads = workpadIds.map(async (id) => {
- try {
- await workpadService.remove(id);
- return { id, err: null };
- } catch (err) {
- return { id, err };
- }
- });
-
- return Promise.all(removedWorkpads).then((results) => {
- let redirectHome = false;
-
- const [passes, errored] = results.reduce<[string[], string[]]>(
- ([passesArr, errorsArr], result) => {
- if (result.id === fromState.workpadId && !result.err) {
- redirectHome = true;
- }
-
- if (result.err) {
- errorsArr.push(result.id);
- } else {
- passesArr.push(result.id);
- }
-
- return [passesArr, errorsArr];
- },
- [[], []]
- );
-
- const remainingWorkpads = workpadsState.workpads.filter(({ id }) => !passes.includes(id));
-
- const workpadState = {
- total: remainingWorkpads.length,
- workpads: remainingWorkpads,
- };
-
- if (errored.length > 0) {
- notifyService.error(errors.getDeleteFailureErrorMessage());
- }
-
- setWorkpadsState(workpadState);
-
- if (redirectHome) {
- history.push('/');
- }
-
- return errored;
- });
- },
- [history, workpadService, fromState.workpadId, workpadsState, notifyService]
- );
-
- const formatDate = useCallback(
- (date: any) => {
- const dateFormat = platformService.getUISetting('dateFormat');
- return date && moment(date).format(dateFormat);
- },
- [platformService]
- );
-
- const { workpadId, canUserWrite } = fromState;
-
- return (
-
- );
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js b/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js
deleted file mode 100644
index 24a694268e4ee..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { get } from 'lodash';
-import { getId } from '../../lib/get_id';
-import { ErrorStrings } from '../../../i18n';
-
-const { WorkpadFileUpload: errors } = ErrorStrings;
-
-export const uploadWorkpad = (file, onUpload, notify) => {
- if (!file) {
- return;
- }
-
- if (get(file, 'type') !== 'application/json') {
- return notify.warning(errors.getAcceptJSONOnlyErrorMessage(), {
- title: file.name
- ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
- : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
- });
- }
- // TODO: Clean up this file, this loading stuff can, and should be, abstracted
- const reader = new FileReader();
-
- // handle reading the uploaded file
- reader.onload = () => {
- try {
- const workpad = JSON.parse(reader.result);
- workpad.id = getId('workpad');
-
- // sanity check for workpad object
- if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) {
- throw new Error(errors.getMissingPropertiesErrorMessage());
- }
-
- onUpload(workpad);
- } catch (e) {
- notify.error(e, {
- title: file.name
- ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
- : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
- });
- }
- };
-
- // read the uploaded file
- reader.readAsText(file);
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js
deleted file mode 100644
index 51733dad5b377..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { EuiButton } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n';
-
-const { WorkpadCreate: strings } = ComponentStrings;
-
-export const WorkpadCreate = ({ createPending, onCreate, ...rest }) => (
-
- {strings.getWorkpadCreateButtonLabel()}
-
-);
-
-WorkpadCreate.propTypes = {
- onCreate: PropTypes.func.isRequired,
- createPending: PropTypes.bool,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js
deleted file mode 100644
index 7c34837771c6f..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import PropTypes from 'prop-types';
-import { compose, withHandlers } from 'recompose';
-import { uploadWorkpad } from '../upload_workpad';
-import { ErrorStrings } from '../../../../i18n';
-import { WorkpadDropzone as Component } from './workpad_dropzone';
-
-const { WorkpadFileUpload: errors } = ErrorStrings;
-
-export const WorkpadDropzone = compose(
- withHandlers(({ notify }) => ({
- onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload),
- onDropRejected: () => ([file]) => {
- notify.warning(errors.getAcceptJSONOnlyErrorMessage(), {
- title: file.name
- ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
- : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
- });
- },
- }))
-)(Component);
-
-WorkpadDropzone.propTypes = {
- onUpload: PropTypes.func.isRequired,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js
deleted file mode 100644
index f77929e1feb76..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import Dropzone from 'react-dropzone';
-
-export const WorkpadDropzone = ({ onDropAccepted, onDropRejected, disabled, children }) => (
-
- {children}
-
-);
-
-WorkpadDropzone.propTypes = {
- onDropAccepted: PropTypes.func.isRequired,
- onDropRejected: PropTypes.func.isRequired,
- disabled: PropTypes.bool.isRequired,
- children: PropTypes.node.isRequired,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss
deleted file mode 100644
index ac6838da97fbd..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-.canvasWorkpad__dropzone {
- border: 2px dashed transparent;
-}
-
-.canvasWorkpad__dropzone--active {
- background-color: $euiColorLightestShade;
- border-color: $euiColorLightShade;
-}
-
-.canvasWorkpad__dropzoneTable .euiTable {
- background-color: transparent;
-}
-
-.canvasWorkpad__dropzoneTable--tags {
- .euiTableCellContent {
- flex-wrap: wrap;
- }
-
- .euiHealth {
- width: 100%;
- }
-}
\ No newline at end of file
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
deleted file mode 100644
index 9c232ab43ec8d..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiBasicTable,
- EuiButtonIcon,
- EuiPagination,
- EuiSpacer,
- EuiButton,
- EuiToolTip,
- EuiEmptyPrompt,
- EuiFilePicker,
- EuiLink,
-} from '@elastic/eui';
-import { orderBy } from 'lodash';
-import { ConfirmModal } from '../confirm_modal';
-import { RoutingLink } from '../routing';
-import { Paginate } from '../paginate';
-import { ComponentStrings } from '../../../i18n';
-import { WorkpadDropzone } from './workpad_dropzone';
-import { WorkpadCreate } from './workpad_create';
-import { WorkpadSearch } from './workpad_search';
-import { uploadWorkpad } from './upload_workpad';
-
-const { WorkpadLoader: strings } = ComponentStrings;
-
-const getDisplayName = (name, workpad, loadedWorkpad) => {
- const workpadName = name.length ? name : {workpad.id} ;
- return workpad.id === loadedWorkpad ? {workpadName} : workpadName;
-};
-
-export class WorkpadLoader extends React.PureComponent {
- static propTypes = {
- workpadId: PropTypes.string.isRequired,
- canUserWrite: PropTypes.bool.isRequired,
- createWorkpad: PropTypes.func.isRequired,
- findWorkpads: PropTypes.func.isRequired,
- downloadWorkpad: PropTypes.func.isRequired,
- cloneWorkpad: PropTypes.func.isRequired,
- removeWorkpads: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- workpads: PropTypes.object,
- formatDate: PropTypes.func.isRequired,
- };
-
- state = {
- createPending: false,
- deletingWorkpad: false,
- sortField: '@timestamp',
- sortDirection: 'desc',
- selectedWorkpads: [],
- pageSize: 10,
- };
-
- async componentDidMount() {
- // on component load, kick off the workpad search
- this.props.findWorkpads();
-
- // keep track of whether or not the component is mounted, to prevent rogue setState calls
- this._isMounted = true;
- }
-
- UNSAFE_componentWillReceiveProps(newProps) {
- // the workpadId prop will change when a is created or loaded, close the toolbar when it does
- const { workpadId, onClose } = this.props;
- if (workpadId !== newProps.workpadId) {
- onClose();
- }
- }
-
- componentWillUnmount() {
- this._isMounted = false;
- }
-
- // create new empty workpad
- createWorkpad = async () => {
- this.setState({ createPending: true });
- await this.props.createWorkpad();
- this._isMounted && this.setState({ createPending: false });
- };
-
- // create new workpad from uploaded JSON
- onUpload = async (workpad) => {
- this.setState({ createPending: true });
- await this.props.createWorkpad(workpad);
- this._isMounted && this.setState({ createPending: false });
- };
-
- // clone existing workpad
- cloneWorkpad = async (workpad) => {
- this.setState({ createPending: true });
- await this.props.cloneWorkpad(workpad.id);
- this._isMounted && this.setState({ createPending: false });
- };
-
- // Workpad remove methods
- openRemoveConfirm = () => this.setState({ deletingWorkpad: true });
-
- closeRemoveConfirm = () => this.setState({ deletingWorkpad: false });
-
- removeWorkpads = () => {
- const { selectedWorkpads } = this.state;
-
- this.props.removeWorkpads(selectedWorkpads.map(({ id }) => id)).then((remainingIds) => {
- const remainingWorkpads =
- remainingIds.length > 0
- ? selectedWorkpads.filter(({ id }) => remainingIds.includes(id))
- : [];
-
- this._isMounted &&
- this.setState({
- deletingWorkpad: false,
- selectedWorkpads: remainingWorkpads,
- });
- });
- };
-
- // downloads selected workpads as JSON files
- downloadWorkpads = () => {
- this.state.selectedWorkpads.forEach(({ id }) => this.props.downloadWorkpad(id));
- };
-
- onSelectionChange = (selectedWorkpads) => {
- this.setState({ selectedWorkpads });
- };
-
- onTableChange = ({ sort = {} }) => {
- const { field: sortField, direction: sortDirection } = sort;
- this.setState({
- sortField,
- sortDirection,
- });
- };
-
- renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }) => {
- const { sortField, sortDirection } = this.state;
- const { canUserWrite, createPending, workpadId: loadedWorkpad } = this.props;
-
- const actions = [
- {
- render: (workpad) => (
-
-
-
- this.props.downloadWorkpad(workpad.id)}
- aria-label={strings.getExportToolTip()}
- />
-
-
-
-
- this.cloneWorkpad(workpad)}
- aria-label={strings.getCloneToolTip()}
- disabled={!canUserWrite}
- />
-
-
-
- ),
- },
- ];
-
- const columns = [
- {
- field: 'name',
- name: strings.getTableNameColumnTitle(),
- sortable: true,
- dataType: 'string',
- render: (name, workpad) => {
- const workpadName = getDisplayName(name, workpad, loadedWorkpad);
-
- return (
-
- {workpadName}
-
- );
- },
- },
- {
- field: '@created',
- name: strings.getTableCreatedColumnTitle(),
- sortable: true,
- dataType: 'date',
- width: '20%',
- render: (date) => this.props.formatDate(date),
- },
- {
- field: '@timestamp',
- name: strings.getTableUpdatedColumnTitle(),
- sortable: true,
- dataType: 'date',
- width: '20%',
- render: (date) => this.props.formatDate(date),
- },
- { name: strings.getTableActionsColumnTitle(), actions, width: '100px' },
- ];
-
- const sorting = {
- sort: {
- field: sortField,
- direction: sortDirection,
- },
- };
-
- const selection = {
- itemId: 'id',
- onSelectionChange: this.onSelectionChange,
- };
-
- const emptyTable = (
- {strings.getEmptyPromptTitle()}}
- titleSize="s"
- body={
-
- {strings.getEmptyPromptGettingStartedDescription()}
-
- {strings.getEmptyPromptNewUserDescription()}{' '}
-
- {strings.getSampleDataLinkLabel()}
-
- .
-
-
- }
- />
- );
-
- return (
-
-
-
-
- {rows.length > 0 && (
-
-
-
-
-
- )}
-
-
- );
- };
-
- render() {
- const {
- deletingWorkpad,
- createPending,
- selectedWorkpads,
- sortField,
- sortDirection,
- } = this.state;
- const { canUserWrite } = this.props;
- const isLoading = this.props.workpads == null;
-
- let createButton = (
-
- );
-
- let deleteButton = (
-
- {strings.getDeleteButtonLabel(selectedWorkpads.length)}
-
- );
-
- const downloadButton = (
-
- {strings.getExportButtonLabel(selectedWorkpads.length)}
-
- );
-
- let uploadButton = (
- uploadWorkpad(file, this.onUpload, this.props.notify)}
- accept="application/json"
- disabled={createPending || !canUserWrite}
- />
- );
-
- if (!canUserWrite) {
- createButton = (
- {createButton}
- );
- deleteButton = (
- {deleteButton}
- );
- uploadButton = (
- {uploadButton}
- );
- }
-
- const modalTitle =
- selectedWorkpads.length === 1
- ? strings.getDeleteSingleWorkpadModalTitle(selectedWorkpads[0].name)
- : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpads.length);
-
- const confirmModal = (
-
- );
-
- let sortedWorkpads = [];
-
- if (!createPending && !isLoading) {
- const { workpads } = this.props.workpads;
- sortedWorkpads = orderBy(workpads, [sortField, '@timestamp'], [sortDirection, 'desc']);
- }
-
- return (
-
- {(pagination) => (
-
-
-
-
- {selectedWorkpads.length > 0 && (
-
- {downloadButton}
- {deleteButton}
-
- )}
-
- {
- pagination.setPage(0);
- this.props.findWorkpads(text);
- }}
- />
-
-
-
-
-
- {uploadButton}
- {createButton}
-
-
-
-
-
-
- {createPending && (
- {strings.getCreateWorkpadLoadingDescription()}
- )}
-
- {!createPending && isLoading && (
- {strings.getFetchLoadingDescription()}
- )}
-
- {!createPending && !isLoading && this.renderWorkpadTable(pagination)}
-
- {confirmModal}
-
- )}
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss
deleted file mode 100644
index 3b2c8eae9e542..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-.canvasWorkpad__upload--compressed {
-
- &.euiFilePicker--compressed.euiFilePicker {
- .euiFilePicker__prompt {
- height: $euiSizeXXL;
- padding: $euiSizeM;
- padding-left: $euiSizeXXL;
- }
-
- .euiFilePicker__icon {
- top: $euiSizeM;
- }
- }
-
- // The file picker input is being used moreso as a button, outside of a form,
- // and thus the need to override the default max-width of form inputs.
- // An issue has been opened in EUI to consider creating a button
- // version of the file picker - https://github.com/elastic/eui/issues/1987
-
- .euiFilePicker__wrap {
- @include euiBreakpoint('xs', 's') {
- max-width: none;
- }
- }
-}
\ No newline at end of file
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js
deleted file mode 100644
index 8bf8bbae8ced4..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { EuiFieldSearch } from '@elastic/eui';
-import { debounce } from 'lodash';
-import { ComponentStrings } from '../../../i18n';
-
-const { WorkpadSearch: strings } = ComponentStrings;
-export class WorkpadSearch extends React.PureComponent {
- static propTypes = {
- onChange: PropTypes.func.isRequired,
- initialText: PropTypes.string,
- };
-
- state = {
- searchText: this.props.initialText || '',
- };
-
- triggerChange = debounce(this.props.onChange, 150);
-
- setSearchText = (ev) => {
- const text = ev.target.value;
- this.setState({ searchText: text });
- this.triggerChange(text);
- };
-
- render() {
- return (
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js
deleted file mode 100644
index 8055be32ac481..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import {
- EuiTabbedContent,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiModalBody,
- EuiSpacer,
- EuiFlexGroup,
- EuiFlexItem,
-} from '@elastic/eui';
-import { WorkpadLoader } from '../workpad_loader';
-import { WorkpadTemplates } from '../workpad_templates';
-import { ComponentStrings } from '../../../i18n';
-
-const { WorkpadManager: strings } = ComponentStrings;
-
-export const WorkpadManager = ({ onClose }) => {
- const tabs = [
- {
- id: 'workpadLoader',
- name: strings.getMyWorkpadsTabLabel(),
- content: (
-
-
-
-
- ),
- },
- {
- id: 'workpadTemplates',
- name: strings.getWorkpadTemplatesTabLabel(),
- 'data-test-subj': 'workpadTemplates',
- content: (
-
-
-
-
- ),
- },
- ];
- return (
-
-
-
-
-
- {strings.getModalTitle()}
-
-
-
-
-
-
-
-
- );
-};
-
-WorkpadManager.propTypes = {
- onClose: PropTypes.func,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
deleted file mode 100644
index cab6e8fd9b5f5..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
+++ /dev/null
@@ -1,564 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Storyshots components/WorkpadTemplates default 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sorting
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Template name
-
-
-
-
-
-
-
-
- Description
-
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
-
-
- Template name
-
-
-
-
-
- test1
-
-
-
-
-
-
-
- Description
-
-
-
- This is a test template
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
- Template name
-
-
-
-
-
- test2
-
-
-
-
-
-
-
- Description
-
-
-
- This is a second test template
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx
deleted file mode 100644
index 8e6c055478ca2..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { storiesOf } from '@storybook/react';
-import { action } from '@storybook/addon-actions';
-import { WorkpadTemplates } from '../workpad_templates';
-import { CanvasTemplate } from '../../../../types';
-
-const templates: Record = {
- test1: {
- id: 'test1-id',
- name: 'test1',
- help: 'This is a test template',
- tags: ['tag1', 'tag2'],
- template_key: 'test1-key',
- },
- test2: {
- id: 'test2-id',
- name: 'test2',
- help: 'This is a second test template',
- tags: ['tag2', 'tag3'],
- template_key: 'test2-key',
- },
-};
-
-storiesOf('components/WorkpadTemplates', module)
- .addDecorator((story) => {story()}
)
- .add('default', () => {
- const onCreateFromTemplateAction = action('onCreateFromTemplate');
- return (
- {
- onCreateFromTemplateAction(template);
- return Promise.resolve();
- }}
- />
- );
- });
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx
deleted file mode 100644
index 7e007b1253464..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useCallback, useState, useEffect, FunctionComponent } from 'react';
-import { EuiLoadingSpinner } from '@elastic/eui';
-import { useHistory } from 'react-router-dom';
-
-import { ComponentStrings } from '../../../i18n/components';
-// @ts-expect-error
-import * as workpadService from '../../lib/workpad_service';
-import { WorkpadTemplates as Component } from './workpad_templates';
-import { CanvasTemplate } from '../../../types';
-import { list } from '../../lib/template_service';
-import { applyTemplateStrings } from '../../../i18n/templates/apply_strings';
-import { useNotifyService, useServices } from '../../services';
-
-interface WorkpadTemplatesProps {
- onClose: () => void;
-}
-
-const Creating: FunctionComponent<{ name: string }> = ({ name }) => (
-
- {' '}
- {ComponentStrings.WorkpadTemplates.getCreatingTemplateLabel(name)}
-
-);
-export const WorkpadTemplates: FunctionComponent = ({ onClose }) => {
- const history = useHistory();
- const services = useServices();
-
- const [templates, setTemplates] = useState(undefined);
- const [creatingFromTemplateName, setCreatingFromTemplateName] = useState(
- undefined
- );
- const { error } = useNotifyService();
-
- useEffect(() => {
- if (!templates) {
- (async () => {
- const fetchedTemplates = await list();
- setTemplates(applyTemplateStrings(fetchedTemplates));
- })();
- }
- }, [templates]);
-
- let templateProp: Record = {};
-
- if (templates) {
- templateProp = templates.reduce>((reduction, template) => {
- reduction[template.name] = template;
- return reduction;
- }, {});
- }
-
- const createFromTemplate = useCallback(
- async (template: CanvasTemplate) => {
- setCreatingFromTemplateName(template.name);
- try {
- const result = await services.workpad.createFromTemplate(template.id);
- history.push(`/workpad/${result.id}/page/1`);
- } catch (e) {
- setCreatingFromTemplateName(undefined);
- error(e, {
- title: `Couldn't create workpad from template`,
- });
- }
- },
- [services.workpad, error, history]
- );
-
- if (creatingFromTemplateName) {
- return ;
- }
-
- return (
-
- );
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx
deleted file mode 100644
index 72871b93c1735..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiBasicTable,
- EuiPagination,
- EuiSpacer,
- EuiButtonEmpty,
- EuiSearchBar,
- EuiTableSortingType,
- Direction,
- SortDirection,
-} from '@elastic/eui';
-import { orderBy } from 'lodash';
-// @ts-ignore untyped local
-import { EuiBasicTableColumn } from '@elastic/eui';
-import { Paginate, PaginateChildProps } from '../paginate';
-import { TagList } from '../tag_list';
-import { getTagsFilter } from '../../lib/get_tags_filter';
-// @ts-expect-error
-import { extractSearch } from '../../lib/extract_search';
-import { ComponentStrings } from '../../../i18n';
-import { CanvasTemplate } from '../../../types';
-
-interface TableChange {
- page?: {
- index: number;
- size: number;
- };
- sort?: {
- field: keyof T;
- direction: Direction;
- };
-}
-
-const { WorkpadTemplates: strings } = ComponentStrings;
-
-interface WorkpadTemplatesProps {
- onCreateFromTemplate: (template: CanvasTemplate) => Promise;
- onClose: () => void;
- templates: Record;
-}
-
-interface WorkpadTemplatesState {
- sortField: string;
- sortDirection: Direction;
- pageSize: number;
- searchTerm: string;
- filterTags: string[];
-}
-
-export class WorkpadTemplates extends React.PureComponent<
- WorkpadTemplatesProps,
- WorkpadTemplatesState
-> {
- static propTypes = {
- onCreateFromTemplate: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- templates: PropTypes.object,
- };
-
- state = {
- sortField: 'name',
- sortDirection: SortDirection.ASC,
- pageSize: 10,
- searchTerm: '',
- filterTags: [],
- };
-
- tagType: 'health' = 'health';
-
- onTableChange = (tableChange: TableChange) => {
- if (tableChange.sort) {
- const { field: sortField, direction: sortDirection } = tableChange.sort;
- this.setState({
- sortField,
- sortDirection,
- });
- }
- };
-
- onSearch = ({ queryText = '' }) => this.setState(extractSearch(queryText));
-
- cloneTemplate = (template: CanvasTemplate) =>
- this.props.onCreateFromTemplate(template).then(() => this.props.onClose());
-
- renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }: PaginateChildProps) => {
- const { sortField, sortDirection } = this.state;
-
- const columns: Array> = [
- {
- field: 'name',
- name: strings.getTableNameColumnTitle(),
- sortable: true,
- width: '30%',
- dataType: 'string',
- render: (name: string, template) => {
- const templateName = name.length ? name : 'Unnamed Template';
-
- return (
- this.cloneTemplate(template)}
- aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)}
- type="button"
- >
- {templateName}
-
- );
- },
- },
- {
- field: 'help',
- name: strings.getTableDescriptionColumnTitle(),
- sortable: false,
- dataType: 'string',
- width: '30%',
- },
- {
- field: 'tags',
- name: strings.getTableTagsColumnTitle(),
- sortable: false,
- dataType: 'string',
- width: '30%',
- render: (tags: string[]) => ,
- },
- ];
-
- const sorting: EuiTableSortingType = {
- sort: {
- field: sortField,
- direction: sortDirection,
- },
- };
-
- return (
-
-
-
- {rows.length > 0 && (
-
-
-
-
-
- )}
-
- );
- };
-
- renderSearch = () => {
- const { searchTerm } = this.state;
- const filters = [getTagsFilter(this.tagType)];
-
- return (
-
- );
- };
-
- render() {
- const { templates } = this.props;
- const { sortField, sortDirection, searchTerm, filterTags } = this.state;
- const sortedTemplates = orderBy(templates, [sortField, 'name'], [sortDirection, 'asc']);
-
- const filteredTemplates = sortedTemplates.filter(({ name = '', help = '', tags = [] }) => {
- const tagMatch = filterTags.length
- ? filterTags.every((filterTag) => tags.indexOf(filterTag) > -1)
- : true;
-
- const lowercaseSearch = searchTerm.toLowerCase();
- const textMatch = lowercaseSearch
- ? name.toLowerCase().indexOf(lowercaseSearch) > -1 ||
- help.toLowerCase().indexOf(lowercaseSearch) > -1
- : true;
-
- return tagMatch && textMatch;
- });
-
- return (
-
- {(pagination: PaginateChildProps) => (
-
- {this.renderSearch()}
-
- {this.renderWorkpadTable(pagination)}
-
- )}
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx b/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx
deleted file mode 100644
index 12d77c9c7f0c0..0000000000000
--- a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { sortBy } from 'lodash';
-import { SearchFilterConfig } from '@elastic/eui';
-import { Tag } from '../components/tag';
-import { getId } from './get_id';
-import { tagsRegistry } from './tags_registry';
-import { ComponentStrings } from '../../i18n';
-
-const { WorkpadTemplates: strings } = ComponentStrings;
-
-// EUI helper function
-// generates the FieldValueSelectionFilter object for EuiSearchBar for tag filtering
-export const getTagsFilter = (type: 'health' | 'badge'): SearchFilterConfig => {
- const uniqueTags = sortBy(Object.values(tagsRegistry.toJS()), 'name');
- const filterType = 'field_value_selection';
-
- return {
- type: filterType,
- field: 'tag',
- name: strings.getTableTagsColumnTitle(),
- multiSelect: true,
- options: uniqueTags.map(({ name, color }) => ({
- value: name,
- name,
- view: (
-
-
-
- ),
- })),
- };
-};
diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts
index 6c039660c64c7..3f8f58367171a 100644
--- a/x-pack/plugins/canvas/public/services/index.ts
+++ b/x-pack/plugins/canvas/public/services/index.ts
@@ -34,7 +34,7 @@ export type CanvasServiceFactory = (
appUpdater: BehaviorSubject
) => Service | Promise;
-class CanvasServiceProvider {
+export class CanvasServiceProvider {
private factory: CanvasServiceFactory;
private service: Service | undefined;
diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts
index ea80a5a7c26b9..5776a1d0d6983 100644
--- a/x-pack/plugins/canvas/public/services/stubs/platform.ts
+++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts
@@ -9,13 +9,19 @@ import { PlatformService } from '../platform';
const noop = (..._args: any[]): any => {};
+const uiSettings: Record = {
+ dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
+};
+
+const getUISetting = (setting: string) => uiSettings[setting];
+
export const platformService: PlatformService = {
getBasePath: () => '/base/path',
getBasePathInterface: noop,
getDocLinkVersion: () => 'dockLinkVersion',
getElasticWebsiteUrl: () => 'https://elastic.co',
getHasWriteAccess: () => true,
- getUISetting: noop,
+ getUISetting,
setBreadcrumbs: noop,
setRecentlyAccessed: noop,
getSavedObjects: noop,
diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
index 857831c92a8a6..4e3612feb67c8 100644
--- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
@@ -5,17 +5,95 @@
* 2.0.
*/
+import moment from 'moment';
+
+// @ts-expect-error
+import { getDefaultWorkpad } from '../../state/defaults';
import { WorkpadService } from '../workpad';
-import { CanvasWorkpad } from '../../../types';
+import { getId } from '../../lib/get_id';
+import { CanvasTemplate } from '../../../types';
-export const workpadService: WorkpadService = {
- get: (id: string) => Promise.resolve({} as CanvasWorkpad),
- create: (workpad) => Promise.resolve({} as CanvasWorkpad),
- createFromTemplate: (templateId: string) => Promise.resolve({} as CanvasWorkpad),
- find: (term: string) =>
- Promise.resolve({
+const TIMEOUT = 500;
+
+const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time));
+const getName = () => {
+ const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split(
+ ' '
+ );
+ return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' ');
+};
+
+const randomDate = (
+ start: Date = moment().toDate(),
+ end: Date = moment().subtract(7, 'days').toDate()
+) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString();
+
+const templates: CanvasTemplate[] = [
+ {
+ id: 'test1-id',
+ name: 'test1',
+ help: 'This is a test template',
+ tags: ['tag1', 'tag2'],
+ template_key: 'test1-key',
+ },
+ {
+ id: 'test2-id',
+ name: 'test2',
+ help: 'This is a second test template',
+ tags: ['tag2', 'tag3'],
+ template_key: 'test2-key',
+ },
+];
+
+export const getSomeWorkpads = (count = 3) =>
+ Array.from({ length: count }, () => ({
+ '@created': randomDate(
+ moment().subtract(3, 'days').toDate(),
+ moment().subtract(10, 'days').toDate()
+ ),
+ '@timestamp': randomDate(),
+ id: getId('workpad'),
+ name: getName(),
+ }));
+
+export const findSomeWorkpads = (count = 3, timeout = TIMEOUT) => (_term: string) => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => ({
+ total: count,
+ workpads: getSomeWorkpads(count),
+ }));
+};
+
+export const findNoWorkpads = (timeout = TIMEOUT) => (_term: string) => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => ({
total: 0,
workpads: [],
- }),
- remove: (id: string) => Promise.resolve(undefined),
+ }));
+};
+
+export const findSomeTemplates = (timeout = TIMEOUT) => () => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => getSomeTemplates());
+};
+
+export const findNoTemplates = (timeout = TIMEOUT) => () => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => getNoTemplates());
+};
+
+export const getNoTemplates = () => ({ templates: [] });
+export const getSomeTemplates = () => ({ templates });
+
+export const workpadService: WorkpadService = {
+ get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }),
+ findTemplates: findNoTemplates(),
+ create: (workpad) => Promise.resolve(workpad),
+ createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()),
+ find: findNoWorkpads(),
+ remove: (id: string) => Promise.resolve(),
};
diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts
index 11690ca4c0c45..7d2f1550a312f 100644
--- a/x-pack/plugins/canvas/public/services/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/workpad.ts
@@ -5,8 +5,12 @@
* 2.0.
*/
-import { API_ROUTE_WORKPAD, DEFAULT_WORKPAD_CSS } from '../../common/lib/constants';
-import { CanvasWorkpad } from '../../types';
+import {
+ API_ROUTE_WORKPAD,
+ DEFAULT_WORKPAD_CSS,
+ API_ROUTE_TEMPLATES,
+} from '../../common/lib/constants';
+import { CanvasWorkpad, CanvasTemplate } from '../../types';
import { CanvasServiceFactory } from './';
/*
@@ -40,9 +44,15 @@ const sanitizeWorkpad = function (workpad: CanvasWorkpad) {
return workpad;
};
-interface WorkpadFindResponse {
+export type FoundWorkpads = Array>;
+export type FoundWorkpad = FoundWorkpads[number];
+export interface WorkpadFindResponse {
total: number;
- workpads: Array>;
+ workpads: FoundWorkpads;
+}
+
+export interface TemplateFindResponse {
+ templates: CanvasTemplate[];
}
export interface WorkpadService {
@@ -51,6 +61,7 @@ export interface WorkpadService {
createFromTemplate: (templateId: string) => Promise;
find: (term: string) => Promise;
remove: (id: string) => Promise;
+ findTemplates: () => Promise;
}
export const workpadServiceFactory: CanvasServiceFactory = (
@@ -82,7 +93,9 @@ export const workpadServiceFactory: CanvasServiceFactory = (
body: JSON.stringify({ templateId }),
});
},
+ findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES),
find: (searchTerm: string) => {
+ // TODO: this shouldn't be necessary. Check for usage.
const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0;
return coreStart.http.get(`${getApiPath()}/find`, {
diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss
index a79e07a7d0016..d9592d5c0be5f 100644
--- a/x-pack/plugins/canvas/public/style/index.scss
+++ b/x-pack/plugins/canvas/public/style/index.scss
@@ -40,8 +40,6 @@
@import '../components/workpad_header/element_menu/element_menu';
@import '../components/workpad_header/share_menu/share_menu';
@import '../components/workpad_header/view_menu/view_menu';
-@import '../components/workpad_loader/workpad_loader';
-@import '../components/workpad_loader/workpad_dropzone/workpad_dropzone';
@import '../components/workpad_page/workpad_page';
@import '../components/workpad_page/workpad_interactive_page/workpad_interactive_page';
@import '../components/workpad_page/workpad_static_page/workpad_static_page';
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap
index 621a6bb211fe9..27f0d3610fb9f 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap
+++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap
@@ -294,7 +294,7 @@ exports[` can navigate Autoplay Settings 2`] = `
class="euiFormControlLayout__childrenWrapper"
>
can navigate Autoplay Settings 2`] = `
Use shorthand notation, like 30s, 10m, or 1h
@@ -585,7 +585,7 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] =
>
can navigate Toolbar Settings, closes when activated 2`] =
Hide the toolbar when the mouse is not within the Canvas?
@@ -640,4 +640,4 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] =
`;
-exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
You are in a dialog. To close this dialog, hit escape.
"`;
+exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
You are in a dialog. To close this dialog, hit escape.
"`;
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot
index f5823874db73e..b32ae3fc2f49f 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot
+++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot
@@ -101,7 +101,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: off,
className="euiFormControlLayout__childrenWrapper"
>
Use shorthand notation, like 30s, 10m, or 1h
@@ -264,7 +264,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5
className="euiFormControlLayout__childrenWrapper"
>
Use shorthand notation, like 30s, 10m, or 1h
@@ -427,7 +427,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] =
className="euiFormControlLayout__childrenWrapper"
>
Use shorthand notation, like 30s, 10m, or 1h
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot
index a8a3f584bbf51..1aafb9cc6b664 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot
+++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot
@@ -34,7 +34,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1`
>
Hide the toolbar when the mouse is not within the Canvas?
@@ -122,7 +122,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: on 1`]
>
Hide the toolbar when the mouse is not within the Canvas?
@@ -210,7 +210,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings contextual 1`] =
>
Hide the toolbar when the mouse is not within the Canvas?
diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts
index a674eaad576a7..598a2333be554 100644
--- a/x-pack/plugins/canvas/storybook/decorators/index.ts
+++ b/x-pack/plugins/canvas/storybook/decorators/index.ts
@@ -11,6 +11,7 @@ import { kibanaContextDecorator } from './kibana_decorator';
import { servicesContextDecorator } from './services_decorator';
export { reduxDecorator } from './redux_decorator';
+export { servicesContextDecorator } from './services_decorator';
export const addDecorators = () => {
if (process.env.NODE_ENV === 'test') {
@@ -20,5 +21,5 @@ export const addDecorators = () => {
addDecorator(kibanaContextDecorator);
addDecorator(routerContextDecorator);
- addDecorator(servicesContextDecorator);
+ addDecorator(servicesContextDecorator());
};
diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
index 01d96cb0c70e6..289171f136ab5 100644
--- a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
+++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
@@ -25,7 +25,7 @@ elementsRegistry.register(image);
import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state';
export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants';
-interface Params {
+export interface Params {
workpad?: CanvasWorkpad;
elements?: CanvasElement[];
assets?: CanvasAsset[];
diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
index a11492387ea7f..def5a5681a8c4 100644
--- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
+++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
@@ -7,8 +7,40 @@
import React from 'react';
-import { ServicesProvider } from '../../public/services';
+import {
+ CanvasServiceFactory,
+ CanvasServiceProvider,
+ ServicesProvider,
+} from '../../public/services';
+import {
+ findNoWorkpads,
+ findSomeWorkpads,
+ workpadService,
+ findSomeTemplates,
+ findNoTemplates,
+} from '../../public/services/stubs/workpad';
+import { WorkpadService } from '../../public/services/workpad';
-export const servicesContextDecorator = (story: Function) => (
- {story()}
-);
+interface Params {
+ findWorkpads?: number;
+ findTemplates?: boolean;
+}
+
+export const servicesContextDecorator = ({
+ findWorkpads = 0,
+ findTemplates: findTemplatesOption = false,
+}: Params = {}) => {
+ const workpadServiceFactory: CanvasServiceFactory = (): WorkpadService => ({
+ ...workpadService,
+ find: findWorkpads > 0 ? findSomeWorkpads(findWorkpads) : findNoWorkpads(),
+ findTemplates: findTemplatesOption ? findSomeTemplates() : findNoTemplates(),
+ });
+
+ const workpad = new CanvasServiceProvider(workpadServiceFactory);
+ // @ts-expect-error This is a hack at the moment, until we can get Canvas moved over to the new services architecture.
+ workpad.start();
+
+ return (story: Function) => (
+ {story()}
+ );
+};
diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts
index 148af337d7720..ff60b84c88a69 100644
--- a/x-pack/plugins/canvas/storybook/index.ts
+++ b/x-pack/plugins/canvas/storybook/index.ts
@@ -10,3 +10,8 @@ import { ACTIONS_PANEL_ID } from './addon/src/constants';
export * from './decorators';
export { ACTIONS_PANEL_ID } from './addon/src/constants';
export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } });
+export const getDisableStoryshotsParameter = () => ({
+ storyshots: {
+ disable: true,
+ },
+});
diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts
index 80a8aeb14a804..69c05322cf3f0 100644
--- a/x-pack/plugins/canvas/storybook/main.ts
+++ b/x-pack/plugins/canvas/storybook/main.ts
@@ -53,6 +53,11 @@ const canvasWebpack = {
},
],
},
+ resolve: {
+ alias: {
+ 'src/plugins': resolve(KIBANA_ROOT, 'src/plugins'),
+ },
+ },
};
module.exports = {
diff --git a/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot
new file mode 100644
index 0000000000000..39ec1e234ead5
--- /dev/null
+++ b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot
@@ -0,0 +1,65 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Home/Empty Prompt Empty Prompt 1`] = `
+
+
+
+
+
+
+
+
+ Add your first workpad
+
+
+
+
+ Create a new workpad, start from a template, or import a workpad JSON file by dropping it here.
+
+
+ New to Canvas?
+
+
+ Add your first workpad
+
+ .
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx
index 0c3765812066e..7f0ea077c7569 100644
--- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx
+++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx
@@ -90,6 +90,11 @@ import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'
jest.mock('@elastic/eui/test-env/components/observer/observer');
EuiObserver.mockImplementation(() => 'EuiObserver');
+// @ts-expect-error untyped library
+import Dropzone from 'react-dropzone';
+jest.mock('react-dropzone');
+Dropzone.mockImplementation(() => 'Dropzone');
+
// This element uses a `ref` and cannot be rendered by Jest snapshots.
import { RenderedElement } from '../shareable_runtime/components/rendered_element';
jest.mock('../shareable_runtime/components/rendered_element');
@@ -111,7 +116,7 @@ addSerializer(styleSheetSerializer);
// Initialize Storyshots and build the Jest Snapshots
initStoryshots({
- configPath: path.resolve(__dirname, './../storybook'),
+ configPath: path.resolve(__dirname),
framework: 'react',
test: multiSnapshotWithOptions({}),
// Don't snapshot tests that start with 'redux'
diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md
index a1660911567da..cfff8c79ee2d4 100644
--- a/x-pack/plugins/cases/README.md
+++ b/x-pack/plugins/cases/README.md
@@ -215,7 +215,7 @@ This action type has no `secrets` properties.
| -------- | ------------------------------------------------------------------------------------------------- | ----------------- |
| id | ID of the connector used for pushing case updates to external systems. | string |
| name | The connector name. | string |
-| type | The type of the connector. Must be one of these: `.servicenow`, `jira`, `.resilient`, and `.none` | string |
+| type | The type of the connector. Must be one of these: `.servicenow`, `.servicenow-sir`, `.swimlane`, `jira`, `.resilient`, and `.none` | string |
| fields | Object containing the connector’s fields. | [fields](#fields) |
#### `fields`
diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts
index 2a81396025d9a..cee432b17933b 100644
--- a/x-pack/plugins/cases/common/api/connectors/index.ts
+++ b/x-pack/plugins/cases/common/api/connectors/index.ts
@@ -12,12 +12,14 @@ import { JiraFieldsRT } from './jira';
import { ResilientFieldsRT } from './resilient';
import { ServiceNowITSMFieldsRT } from './servicenow_itsm';
import { ServiceNowSIRFieldsRT } from './servicenow_sir';
+import { SwimlaneFieldsRT } from './swimlane';
export * from './jira';
export * from './servicenow_itsm';
export * from './servicenow_sir';
export * from './resilient';
export * from './mappings';
+export * from './swimlane';
export type ActionConnector = ActionResult;
export type ActionTypeConnector = ActionType;
@@ -32,10 +34,11 @@ export const ConnectorFieldsRt = rt.union([
export enum ConnectorTypes {
jira = '.jira',
+ none = '.none',
resilient = '.resilient',
serviceNowITSM = '.servicenow',
serviceNowSIR = '.servicenow-sir',
- none = '.none',
+ swimlane = '.swimlane',
}
export const connectorTypes = Object.values(ConnectorTypes);
@@ -55,6 +58,11 @@ const ConnectorServiceNowITSMTypeFieldsRt = rt.type({
fields: rt.union([ServiceNowITSMFieldsRT, rt.null]),
});
+const ConnectorSwimlaneTypeFieldsRt = rt.type({
+ type: rt.literal(ConnectorTypes.swimlane),
+ fields: rt.union([SwimlaneFieldsRT, rt.null]),
+});
+
const ConnectorServiceNowSIRTypeFieldsRt = rt.type({
type: rt.literal(ConnectorTypes.serviceNowSIR),
fields: rt.union([ServiceNowSIRFieldsRT, rt.null]),
@@ -67,10 +75,11 @@ const ConnectorNoneTypeFieldsRt = rt.type({
export const ConnectorTypeFieldsRt = rt.union([
ConnectorJiraTypeFieldsRt,
+ ConnectorNoneTypeFieldsRt,
ConnectorResillientTypeFieldsRt,
ConnectorServiceNowITSMTypeFieldsRt,
ConnectorServiceNowSIRTypeFieldsRt,
- ConnectorNoneTypeFieldsRt,
+ ConnectorSwimlaneTypeFieldsRt,
]);
export const CaseConnectorRt = rt.intersection([
@@ -85,6 +94,7 @@ export type CaseConnector = rt.TypeOf;
export type ConnectorTypeFields = rt.TypeOf;
export type ConnectorJiraTypeFields = rt.TypeOf;
export type ConnectorResillientTypeFields = rt.TypeOf;
+export type ConnectorSwimlaneTypeFields = rt.TypeOf;
export type ConnectorServiceNowITSMTypeFields = rt.TypeOf<
typeof ConnectorServiceNowITSMTypeFieldsRt
>;
diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts
index e0fdd2d7e62dc..8737a6c5a6462 100644
--- a/x-pack/plugins/cases/common/api/connectors/mappings.ts
+++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts
@@ -48,9 +48,6 @@ const ConnectorFieldRt = rt.type({
export type ConnectorField = rt.TypeOf;
-const GetFieldsResponseRt = rt.type({
- defaultMappings: rt.array(ConnectorMappingsAttributesRT),
- fields: rt.array(ConnectorFieldRt),
-});
+const GetDefaultMappingsResponseRt = rt.array(ConnectorMappingsAttributesRT);
-export type GetFieldsResponse = rt.TypeOf;
+export type GetDefaultMappingsResponse = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts
new file mode 100644
index 0000000000000..bc4d9df9ae6a0
--- /dev/null
+++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as rt from 'io-ts';
+
+// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts
+export const SwimlaneFieldsRT = rt.type({
+ caseId: rt.union([rt.string, rt.null]),
+});
+
+export enum SwimlaneConnectorType {
+ All = 'all',
+ Alerts = 'alerts',
+ Cases = 'cases',
+}
+
+export type SwimlaneFieldsType = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts
index 317fe1d8ed144..5d7ee47bb8ea0 100644
--- a/x-pack/plugins/cases/common/constants.ts
+++ b/x-pack/plugins/cases/common/constants.ts
@@ -4,6 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { ConnectorTypes } from './api';
+
export const DEFAULT_DATE_FORMAT = 'dateFormat';
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz';
@@ -59,16 +61,12 @@ export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts`;
export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = '/api/actions/list_action_types';
-export const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow';
-export const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir';
-export const JIRA_ACTION_TYPE_ID = '.jira';
-export const RESILIENT_ACTION_TYPE_ID = '.resilient';
-
export const SUPPORTED_CONNECTORS = [
- SERVICENOW_ITSM_ACTION_TYPE_ID,
- SERVICENOW_SIR_ACTION_TYPE_ID,
- JIRA_ACTION_TYPE_ID,
- RESILIENT_ACTION_TYPE_ID,
+ `${ConnectorTypes.serviceNowITSM}`,
+ `${ConnectorTypes.serviceNowSIR}`,
+ `${ConnectorTypes.jira}`,
+ `${ConnectorTypes.resilient}`,
+ `${ConnectorTypes.swimlane}`,
];
/**
diff --git a/x-pack/plugins/cases/public/common/shared_imports.ts b/x-pack/plugins/cases/public/common/shared_imports.ts
index 675204076b02a..4641fcfa2167c 100644
--- a/x-pack/plugins/cases/public/common/shared_imports.ts
+++ b/x-pack/plugins/cases/public/common/shared_imports.ts
@@ -24,6 +24,8 @@ export {
ValidationError,
ValidationFunc,
VALIDATION_TYPES,
+ FieldConfig,
+ ValidationConfig,
} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export {
Field,
diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx
index 55de4d07b13b9..1fafbac50c2b9 100644
--- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx
@@ -608,6 +608,7 @@ describe('CaseView ', () => {
).toBe(connectorName);
});
});
+
it('should update connector', async () => {
const wrapper = mount(
@@ -628,15 +629,19 @@ describe('CaseView ', () => {
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
-
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
- await waitFor(() => wrapper.update());
+ await waitFor(() => {
+ wrapper.update();
+ expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy();
+ });
+
wrapper.find(`button[data-test-subj="edit-connectors-submit"]`).first().simulate('click');
await waitFor(() => {
- const updateObject = updateCaseProperty.mock.calls[0][0];
+ wrapper.update();
expect(updateCaseProperty).toHaveBeenCalledTimes(1);
+ const updateObject = updateCaseProperty.mock.calls[0][0];
expect(updateObject.updateKey).toEqual('connector');
expect(updateObject.updateValue).toEqual({
id: 'resilient-2',
diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx
index 05f1c6727b168..9c6e9442c8f56 100644
--- a/x-pack/plugins/cases/public/components/case_view/index.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/index.tsx
@@ -31,17 +31,14 @@ import { useGetCaseUserActions } from '../../containers/use_get_case_user_action
import { usePushToService } from '../use_push_to_service';
import { EditConnector } from '../edit_connector';
import { useConnectors } from '../../containers/configure/use_connectors';
-import {
- getConnectorById,
- normalizeActionConnector,
- getNoneConnector,
-} from '../configure_cases/utils';
+import { normalizeActionConnector, getNoneConnector } from '../configure_cases/utils';
import { StatusActionButton } from '../status/button';
import * as i18n from './translations';
import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context';
import { useTimelineContext } from '../timeline_context/use_timeline_context';
import { CasesNavigation } from '../links';
import { OwnerProvider } from '../owner_context';
+import { getConnectorById } from '../utils';
import { DoesNotExist } from './does_not_exist';
const gutterTimeline = '70px'; // seems to be a timeline reference from the original file
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
index 3ee4bc77cd237..ac43ec05319a0 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
@@ -24,15 +24,11 @@ import { ActionConnectorTableItem } from '../../../../triggers_actions_ui/public
import { SectionWrapper } from '../wrappers';
import { Connectors } from './connectors';
import { ClosureOptions } from './closure_options';
-import {
- getConnectorById,
- getNoneConnector,
- normalizeActionConnector,
- normalizeCaseConnector,
-} from './utils';
+import { getNoneConnector, normalizeActionConnector, normalizeCaseConnector } from './utils';
import * as i18n from './translations';
import { Owner } from '../../types';
import { OwnerProvider } from '../owner_context';
+import { getConnectorById } from '../utils';
const FormWrapper = styled.div`
${({ theme }) => css`
diff --git a/x-pack/plugins/cases/public/components/configure_cases/utils.ts b/x-pack/plugins/cases/public/components/configure_cases/utils.ts
index ade1a5e0c2bba..6597417b5068a 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/utils.ts
+++ b/x-pack/plugins/cases/public/components/configure_cases/utils.ts
@@ -10,10 +10,10 @@ import {
CaseField,
ActionType,
ThirdPartyField,
- ActionConnector,
CaseConnector,
CaseConnectorMapping,
} from '../../containers/configure/types';
+import { CaseActionConnector } from '../types';
export const setActionTypeToMapping = (
caseField: CaseField,
@@ -54,13 +54,8 @@ export const getNoneConnector = (): CaseConnector => ({
fields: null,
});
-export const getConnectorById = (
- id: string,
- connectors: ActionConnector[]
-): ActionConnector | null => connectors.find((c) => c.id === id) ?? null;
-
export const normalizeActionConnector = (
- actionConnector: ActionConnector,
+ actionConnector: CaseActionConnector,
fields: CaseConnector['fields'] = null
): CaseConnector => {
const caseConnectorFieldsType = {
@@ -75,6 +70,6 @@ export const normalizeActionConnector = (
};
export const normalizeCaseConnector = (
- connectors: ActionConnector[],
+ connectors: CaseActionConnector[],
caseConnector: CaseConnector
-): ActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null;
+): CaseActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null;
diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx
index 210334e93adb8..71a65ae030d9d 100644
--- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx
+++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx
@@ -8,6 +8,7 @@
import React, { useCallback } from 'react';
import { isEmpty } from 'lodash/fp';
import { EuiFormRow } from '@elastic/eui';
+import styled from 'styled-components';
import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports';
import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown';
@@ -24,6 +25,13 @@ interface ConnectorSelectorProps {
handleChange?: (newValue: string) => void;
hideConnectorServiceNowSir?: boolean;
}
+
+const EuiFormRowWrapper = styled(EuiFormRow)`
+ .euiFormErrorText {
+ display: none;
+ }
+`;
+
export const ConnectorSelector = ({
connectors,
dataTestSubj,
@@ -47,7 +55,7 @@ export const ConnectorSelector = ({
);
return isEdit ? (
-
-
+
) : null;
};
diff --git a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx
index d71da6f87689d..062695fa41cc2 100644
--- a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx
+++ b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx
@@ -8,7 +8,8 @@
import React, { memo, Suspense } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
-import { CaseActionConnector, ConnectorFieldsProps } from './types';
+import { CaseActionConnector } from '../types';
+import { ConnectorFieldsProps } from './types';
import { getCaseConnectors } from '.';
import { ConnectorTypeFields } from '../../../common';
diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts
index ad202365ae967..3aa10c56dd8e9 100644
--- a/x-pack/plugins/cases/public/components/connectors/index.ts
+++ b/x-pack/plugins/cases/public/components/connectors/index.ts
@@ -8,6 +8,7 @@
import { CaseConnectorsRegistry } from './types';
import { createCaseConnectorsRegistry } from './connectors_registry';
import { getCaseConnector as getJiraCaseConnector } from './jira';
+import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane';
import { getCaseConnector as getResilientCaseConnector } from './resilient';
import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow';
import {
@@ -15,6 +16,7 @@ import {
ServiceNowITSMFieldsType,
ServiceNowSIRFieldsType,
ResilientFieldsType,
+ SwimlaneFieldsType,
} from '../../../common';
export { getActionType as getCaseConnectorUi } from './case';
@@ -40,6 +42,7 @@ class CaseConnectors {
getServiceNowITSMCaseConnector()
);
this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector());
+ this.caseConnectorsRegistry.register(getSwimlaneCaseConnector());
}
registry(): CaseConnectorsRegistry {
diff --git a/x-pack/plugins/cases/public/components/connectors/jira/index.ts b/x-pack/plugins/cases/public/components/connectors/jira/index.ts
index f987d9823af8e..d59d20177c14d 100644
--- a/x-pack/plugins/cases/public/components/connectors/jira/index.ts
+++ b/x-pack/plugins/cases/public/components/connectors/jira/index.ts
@@ -8,13 +8,13 @@
import { lazy } from 'react';
import { CaseConnector } from '../types';
-import { JiraFieldsType } from '../../../../common';
+import { ConnectorTypes, JiraFieldsType } from '../../../../common';
import * as i18n from './translations';
export * from './types';
export const getCaseConnector = (): CaseConnector => ({
- id: '.jira',
+ id: ConnectorTypes.jira,
fieldsComponent: lazy(() => import('./case_fields')),
});
export const fieldLabels = {
diff --git a/x-pack/plugins/cases/public/components/connectors/mock.ts b/x-pack/plugins/cases/public/components/connectors/mock.ts
index f5429fa2396aa..663b397e6f4fe 100644
--- a/x-pack/plugins/cases/public/components/connectors/mock.ts
+++ b/x-pack/plugins/cases/public/components/connectors/mock.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { SwimlaneConnectorType } from '../../../common';
+
export const connector = {
id: '123',
name: 'My connector',
@@ -13,6 +15,22 @@ export const connector = {
isPreconfigured: false,
};
+export const swimlaneConnector = {
+ id: '123',
+ name: 'My connector',
+ actionTypeId: '.swimlane',
+ config: {
+ connectorType: SwimlaneConnectorType.Cases,
+ mappings: {
+ caseIdConfig: {},
+ caseNameConfig: {},
+ descriptionConfig: {},
+ commentsConfig: {},
+ },
+ },
+ isPreconfigured: false,
+};
+
export const issues = [
{ id: 'personId', title: 'Person Task', key: 'personKey' },
{ id: 'womanId', title: 'Woman Task', key: 'womanKey' },
diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts
index 9bf96b16f358c..8a429c0dea091 100644
--- a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts
+++ b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts
@@ -8,13 +8,13 @@
import { lazy } from 'react';
import { CaseConnector } from '../types';
-import { ResilientFieldsType } from '../../../../common';
+import { ConnectorTypes, ResilientFieldsType } from '../../../../common';
import * as i18n from './translations';
export * from './types';
export const getCaseConnector = (): CaseConnector => ({
- id: '.resilient',
+ id: ConnectorTypes.resilient,
fieldsComponent: lazy(() => import('./case_fields')),
});
diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts
index 9df5f87b416e1..88afd902ccf60 100644
--- a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts
+++ b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts
@@ -8,16 +8,20 @@
import { lazy } from 'react';
import { CaseConnector } from '../types';
-import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType } from '../../../../common';
+import {
+ ConnectorTypes,
+ ServiceNowITSMFieldsType,
+ ServiceNowSIRFieldsType,
+} from '../../../../common';
import * as i18n from './translations';
export const getServiceNowITSMCaseConnector = (): CaseConnector => ({
- id: '.servicenow',
+ id: ConnectorTypes.serviceNowITSM,
fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')),
});
export const getServiceNowSIRCaseConnector = (): CaseConnector => ({
- id: '.servicenow-sir',
+ id: ConnectorTypes.serviceNowSIR,
fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')),
});
diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx
new file mode 100644
index 0000000000000..1a035d92611bd
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { SwimlaneConnectorType } from '../../../../common';
+import Fields from './case_fields';
+import * as i18n from './translations';
+import { swimlaneConnector as connector } from '../mock';
+
+const fields = {
+ caseId: '123',
+};
+
+const onChange = jest.fn();
+
+describe('Swimlane Cases Fields', () => {
+ test('it does not shows the mapping error callout', () => {
+ render( );
+ expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeFalsy();
+ });
+
+ test('it shows the mapping error callout when mapping is invalid', () => {
+ const invalidConnector = {
+ ...connector,
+ config: {
+ ...connector.config,
+ mappings: {},
+ },
+ };
+
+ render( );
+ expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy();
+ });
+
+ test('it shows the mapping error callout when the connector is of type alerts', () => {
+ const invalidConnector = {
+ ...connector,
+ config: {
+ ...connector.config,
+ connectorType: SwimlaneConnectorType.Alerts,
+ },
+ };
+
+ render( );
+ expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx
new file mode 100644
index 0000000000000..b6370504edbb6
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import * as i18n from './translations';
+
+import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common';
+import { ConnectorFieldsProps } from '../types';
+import { ConnectorCard } from '../card';
+import { connectorValidator } from './validator';
+
+const SwimlaneComponent: React.FunctionComponent> = ({
+ connector,
+ isEdit = true,
+}) => {
+ const showMappingWarning = useMemo(() => connectorValidator(connector) != null, [connector]);
+
+ return (
+ <>
+ {!isEdit && (
+
+ )}
+ {showMappingWarning && (
+
+ {i18n.EMPTY_MAPPING_WARNING_DESC}
+
+ )}
+ >
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { SwimlaneComponent as default };
diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts
new file mode 100644
index 0000000000000..bd2eaae9e0174
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { lazy } from 'react';
+
+import { CaseConnector } from '../types';
+import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common';
+import * as i18n from './translations';
+
+export const getCaseConnector = (): CaseConnector => {
+ return {
+ id: ConnectorTypes.swimlane,
+ fieldsComponent: lazy(() => import('./case_fields')),
+ };
+};
+
+export const fieldLabels = {
+ caseId: i18n.CASE_ID_LABEL,
+ caseName: i18n.CASE_NAME_LABEL,
+ severity: i18n.SEVERITY_LABEL,
+};
diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts
new file mode 100644
index 0000000000000..eb6cd168fab99
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ALERT_SOURCE_LABEL = i18n.translate(
+ 'xpack.cases.connectors.swimlane.alertSourceLabel',
+ {
+ defaultMessage: 'Alert Source',
+ }
+);
+
+export const CASE_ID_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseIdLabel', {
+ defaultMessage: 'Case Id',
+});
+
+export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseNameLabel', {
+ defaultMessage: 'Case Name',
+});
+
+export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.swimlane.severityLabel', {
+ defaultMessage: 'Severity',
+});
+
+export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate(
+ 'xpack.cases.connectors.swimlane.emptyMappingWarningTitle',
+ {
+ defaultMessage: 'This connector has missing field mappings',
+ }
+);
+
+export const EMPTY_MAPPING_WARNING_DESC = i18n.translate(
+ 'xpack.cases.connectors.swimlane.emptyMappingWarningDesc',
+ {
+ defaultMessage:
+ 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Cases.',
+ }
+);
diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts
new file mode 100644
index 0000000000000..552d988c26330
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SwimlaneConnectorType } from '../../../../common';
+import { swimlaneConnector as connector } from '../mock';
+import { isAnyRequiredFieldNotSet, connectorValidator } from './validator';
+
+describe('Swimlane validator', () => {
+ describe('isAnyRequiredFieldNotSet', () => {
+ test('it returns true if a required field is not set', () => {
+ expect(isAnyRequiredFieldNotSet({ notRequired: 'test' })).toBeTruthy();
+ });
+
+ test('it returns false if all required fields are set', () => {
+ expect(isAnyRequiredFieldNotSet(connector.config.mappings)).toBeFalsy();
+ });
+ });
+
+ describe('connectorValidator', () => {
+ test('it returns an error message if the mapping is not correct', () => {
+ const invalidConnector = {
+ ...connector,
+ config: {
+ ...connector.config,
+ mappings: {},
+ },
+ };
+ expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' });
+ });
+
+ test('it returns an error message if the connector is of type alerts', () => {
+ const invalidConnector = {
+ ...connector,
+ config: {
+ ...connector.config,
+ connectorType: SwimlaneConnectorType.Alerts,
+ },
+ };
+ expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' });
+ });
+
+ test.each([SwimlaneConnectorType.Cases, SwimlaneConnectorType.All])(
+ 'it does not return an error message if the connector is of type %s',
+ (connectorType) => {
+ const invalidConnector = {
+ ...connector,
+ config: {
+ ...connector.config,
+ connectorType,
+ },
+ };
+ expect(connectorValidator(invalidConnector)).toBe(undefined);
+ }
+ );
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts
new file mode 100644
index 0000000000000..4ead75e5854f9
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SwimlaneConnectorType } from '../../../../common';
+import { ValidationConfig } from '../../../common/shared_imports';
+import { CaseActionConnector } from '../../types';
+
+const casesRequiredFields = [
+ 'caseIdConfig',
+ 'caseNameConfig',
+ 'descriptionConfig',
+ 'commentsConfig',
+];
+
+export const isAnyRequiredFieldNotSet = (mapping: Record | undefined) =>
+ casesRequiredFields.some((field) => mapping?.[field] == null);
+
+/**
+ * The user can use either a connector of type cases or all.
+ * If the connector is of type all we should check if all
+ * required field have been configured.
+ */
+
+export const connectorValidator = (
+ connector: CaseActionConnector
+): ReturnType => {
+ const {
+ config: { mappings, connectorType },
+ } = connector;
+ if (connectorType === SwimlaneConnectorType.Alerts || isAnyRequiredFieldNotSet(mappings)) {
+ return {
+ message: 'Invalid connector',
+ };
+ }
+};
diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts
index 4eb97513b9f58..5bbd77c790901 100644
--- a/x-pack/plugins/cases/public/components/connectors/types.ts
+++ b/x-pack/plugins/cases/public/components/connectors/types.ts
@@ -11,12 +11,11 @@ import React from 'react';
import {
ActionType as ThirdPartySupportedActions,
CaseField,
- ActionConnector,
ConnectorTypeFields,
} from '../../../common';
+import { CaseActionConnector } from '../types';
export { ThirdPartyField as AllThirdPartyFields } from '../../../common';
-export type CaseActionConnector = ActionConnector;
export interface ThirdPartyField {
label: string;
diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx
index c453838f6cd7a..bc6d5c8717ece 100644
--- a/x-pack/plugins/cases/public/components/create/connector.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx
@@ -18,6 +18,9 @@ import { useGetSeverity } from '../connectors/resilient/use_get_severity';
import { useGetChoices } from '../connectors/servicenow/use_get_choices';
import { incidentTypes, severity, choices } from '../connectors/mock';
import { schema, FormProps } from './schema';
+import { TestProviders } from '../../common/mock';
+import { useCaseConfigure } from '../../containers/configure/use_configure';
+import { useCaseConfigureResponse } from '../configure_cases/__mock__';
jest.mock('../../common/lib/kibana', () => ({
useKibana: () => ({
@@ -39,10 +42,12 @@ jest.mock('../../common/lib/kibana', () => ({
jest.mock('../connectors/resilient/use_get_incident_types');
jest.mock('../connectors/resilient/use_get_severity');
jest.mock('../connectors/servicenow/use_get_choices');
+jest.mock('../../containers/configure/use_configure');
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
const useGetSeverityMock = useGetSeverity as jest.Mock;
const useGetChoicesMock = useGetChoices as jest.Mock;
+const useCaseConfigureMock = useCaseConfigure as jest.Mock;
const useGetIncidentTypesResponse = {
isLoading: false,
@@ -87,35 +92,30 @@ describe('Connector', () => {
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
useGetChoicesMock.mockReturnValue(useGetChoicesResponse);
+ useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
});
it('it renders', async () => {
const wrapper = mount(
-
-
-
+
+
+
+
+
);
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
- expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy();
-
- await waitFor(() => {
- expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe(
- 'My Connector'
- );
- });
-
- await waitFor(() => {
- wrapper.update();
- expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy();
- });
+ // Selected connector is set to none so no fields should be displayed
+ expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeFalsy();
});
it('it is disabled and loading when isLoadingConnectors=true', async () => {
const wrapper = mount(
-
-
-
+
+
+
+
+
);
expect(
@@ -129,9 +129,11 @@ describe('Connector', () => {
it('it is disabled and loading when isLoading=true', async () => {
const wrapper = mount(
-
-
-
+
+
+
+
+
);
expect(
@@ -144,9 +146,11 @@ describe('Connector', () => {
it(`it should change connector`, async () => {
const wrapper = mount(
-
-
-
+
+
+
+
+
);
expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy();
diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx
index 2049f2a083a6f..2ec6d1ffef23d 100644
--- a/x-pack/plugins/cases/public/components/create/connector.tsx
+++ b/x-pack/plugins/cases/public/components/create/connector.tsx
@@ -5,15 +5,22 @@
* 2.0.
*/
-import React, { memo, useCallback } from 'react';
+import React, { memo, useCallback, useMemo, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { ActionConnector, ConnectorTypes } from '../../../common';
-import { UseField, useFormData, FieldHook, useFormContext } from '../../common/shared_imports';
+import { ConnectorTypes, ActionConnector } from '../../../common';
+import {
+ UseField,
+ useFormData,
+ FieldHook,
+ useFormContext,
+ FieldConfig,
+} from '../../common/shared_imports';
import { ConnectorSelector } from '../connector_selector/form';
import { ConnectorFieldsForm } from '../connectors/fields_form';
-import { getConnectorById } from '../configure_cases/utils';
-import { FormProps } from './schema';
+import { FormProps, schema } from './schema';
+import { useCaseConfigure } from '../../containers/configure/use_configure';
+import { getConnectorById, getConnectorsFormValidators } from '../utils';
interface Props {
connectors: ActionConnector[];
@@ -26,6 +33,7 @@ interface ConnectorsFieldProps {
connectors: ActionConnector[];
field: FieldHook;
isEdit: boolean;
+ setErrors: (errors: boolean) => void;
hideConnectorServiceNowSir?: boolean;
}
@@ -33,11 +41,13 @@ const ConnectorFields = ({
connectors,
isEdit,
field,
+ setErrors,
hideConnectorServiceNowSir = false,
}: ConnectorsFieldProps) => {
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
const { setValue } = field;
let connector = getConnectorById(connectorId, connectors) ?? null;
+
if (
connector &&
hideConnectorServiceNowSir &&
@@ -61,18 +71,49 @@ const ConnectorComponent: React.FC = ({
isLoading,
isLoadingConnectors,
}) => {
- const { getFields } = useFormContext();
+ const { getFields, setFieldValue } = useFormContext();
+ const { connector: configurationConnector } = useCaseConfigure();
+
const handleConnectorChange = useCallback(() => {
const { fields } = getFields();
fields.setValue(null);
}, [getFields]);
+ const defaultConnectorId = useMemo(() => {
+ if (
+ hideConnectorServiceNowSir &&
+ configurationConnector.type === ConnectorTypes.serviceNowSIR
+ ) {
+ return 'none';
+ }
+ return connectors.some((connector) => connector.id === configurationConnector.id)
+ ? configurationConnector.id
+ : 'none';
+ }, [
+ configurationConnector.id,
+ configurationConnector.type,
+ connectors,
+ hideConnectorServiceNowSir,
+ ]);
+
+ useEffect(() => setFieldValue('connectorId', defaultConnectorId), [
+ defaultConnectorId,
+ setFieldValue,
+ ]);
+
+ const connectorIdConfig = getConnectorsFormValidators({
+ config: schema.connectorId as FieldConfig,
+ connectors,
+ });
+
return (
{
jest.resetAllMocks();
useGetTagsMock.mockReturnValue({ tags: ['test'] });
useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock });
+ useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
});
it('it renders with steps', async () => {
diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx
index 30a60fb5c1e47..65c102583455a 100644
--- a/x-pack/plugins/cases/public/components/create/form_context.tsx
+++ b/x-pack/plugins/cases/public/components/create/form_context.tsx
@@ -5,23 +5,19 @@
* 2.0.
*/
-import React, { useCallback, useEffect, useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { schema, FormProps } from './schema';
import { Form, useForm } from '../../common/shared_imports';
-import {
- getConnectorById,
- getNoneConnector,
- normalizeActionConnector,
-} from '../configure_cases/utils';
+import { getNoneConnector, normalizeActionConnector } from '../configure_cases/utils';
import { usePostCase } from '../../containers/use_post_case';
import { usePostPushToService } from '../../containers/use_post_push_to_service';
import { useConnectors } from '../../containers/configure/use_connectors';
-import { useCaseConfigure } from '../../containers/configure/use_configure';
import { Case } from '../../containers/types';
-import { CaseType, ConnectorTypes } from '../../../common';
+import { CaseType } from '../../../common';
import { UsePostComment, usePostComment } from '../../containers/use_post_comment';
import { useOwnerContext } from '../owner_context/use_owner_context';
+import { getConnectorById } from '../utils';
const initialCaseValue: FormProps = {
description: '',
@@ -49,28 +45,10 @@ export const FormContext: React.FC = ({
}) => {
const { connectors, loading: isLoadingConnectors } = useConnectors();
const owner = useOwnerContext();
- const { connector: configurationConnector } = useCaseConfigure();
const { postCase } = usePostCase();
const { postComment } = usePostComment();
const { pushCaseToExternalService } = usePostPushToService();
- const connectorId = useMemo(() => {
- if (
- hideConnectorServiceNowSir &&
- configurationConnector.type === ConnectorTypes.serviceNowSIR
- ) {
- return 'none';
- }
- return connectors.some((connector) => connector.id === configurationConnector.id)
- ? configurationConnector.id
- : 'none';
- }, [
- configurationConnector.id,
- configurationConnector.type,
- connectors,
- hideConnectorServiceNowSir,
- ]);
-
const submitCase = useCallback(
async (
{ connectorId: dataConnectorId, fields, syncAlerts = true, ...dataWithoutConnectorId },
@@ -125,9 +103,6 @@ export const FormContext: React.FC = ({
schema,
onSubmit: submitCase,
});
- const { setFieldValue } = form;
- // Set the selected connector to the configuration connector
- useEffect(() => setFieldValue('connectorId', connectorId), [connectorId, setFieldValue]);
const childrenWithExtraProp = useMemo(
() =>
diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx
index 6e6d1a414280e..bea1a46d93760 100644
--- a/x-pack/plugins/cases/public/components/create/schema.tsx
+++ b/x-pack/plugins/cases/public/components/create/schema.tsx
@@ -49,7 +49,9 @@ export const schema: FormSchema = {
label: i18n.CONNECTORS,
defaultValue: 'none',
},
- fields: {},
+ fields: {
+ defaultValue: null,
+ },
syncAlerts: {
helpText: i18n.SYNC_ALERTS_HELP,
type: FIELD_TYPES.TOGGLE,
diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx
index 570f6e34d2528..8057d188b8c04 100644
--- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx
+++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx
@@ -20,15 +20,15 @@ import {
import styled from 'styled-components';
import { noop } from 'lodash/fp';
-import { Form, UseField, useForm } from '../../common/shared_imports';
+import { FieldConfig, Form, UseField, useForm } from '../../common/shared_imports';
import { ActionConnector, ConnectorTypeFields } from '../../../common';
import { ConnectorSelector } from '../connector_selector/form';
import { ConnectorFieldsForm } from '../connectors/fields_form';
-import { getConnectorById } from '../configure_cases/utils';
import { CaseUserActions } from '../../containers/types';
import { schema } from './schema';
import { getConnectorFieldsFromUserActions } from './helpers';
import * as i18n from './translations';
+import { getConnectorById, getConnectorsFormValidators } from '../utils';
export interface EditConnectorProps {
caseFields: ConnectorTypeFields['fields'];
@@ -205,6 +205,11 @@ export const EditConnector = React.memo(
});
}, [dispatch]);
+ const connectorIdConfig = getConnectorsFormValidators({
+ config: schema.connectorId as FieldConfig,
+ connectors,
+ });
+
/**
* if this evaluates to true it means that the connector was likely deleted because the case connector was set to something
* other than none but we don't find it in the list of connectors returned from the actions plugin
@@ -243,6 +248,7 @@ export const EditConnector = React.memo(
)`
+export const Panel = styled(({ loading, ...props }) => )`
position: relative;
${({ loading }) =>
loading &&
diff --git a/x-pack/plugins/cases/public/components/types.ts b/x-pack/plugins/cases/public/components/types.ts
new file mode 100644
index 0000000000000..014afc371e761
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/types.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ActionConnector } from '../../common';
+
+export type CaseActionConnector = ActionConnector;
diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts
new file mode 100644
index 0000000000000..033529c27a2d4
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ConnectorTypes } from '../../common';
+import { FieldConfig, ValidationConfig } from '../common/shared_imports';
+import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator';
+import { CaseActionConnector } from './types';
+
+export const getConnectorById = (
+ id: string,
+ connectors: CaseActionConnector[]
+): CaseActionConnector | null => connectors.find((c) => c.id === id) ?? null;
+
+const validators: Record<
+ string,
+ (connector: CaseActionConnector) => ReturnType
+> = {
+ [ConnectorTypes.swimlane]: swimlaneConnectorValidator,
+};
+
+export const getConnectorsFormValidators = ({
+ connectors = [],
+ config = {},
+}: {
+ connectors: CaseActionConnector[];
+ config: FieldConfig;
+}): FieldConfig => ({
+ ...config,
+ validations: [
+ {
+ validator: ({ value: connectorId }) => {
+ const connector = getConnectorById(connectorId as string, connectors);
+ if (connector != null) {
+ return validators[connector.actionTypeId]?.(connector);
+ }
+ },
+ },
+ ],
+});
diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx
index 4f28d88c14b25..e4ea6d05011a7 100644
--- a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx
+++ b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx
@@ -11,6 +11,7 @@ import { useToasts } from '../common/lib/kibana';
import { getActionLicense } from './api';
import * as i18n from './translations';
import { ActionLicense } from './types';
+import { ConnectorTypes } from '../../common';
export interface ActionLicenseState {
actionLicense: ActionLicense | null;
@@ -24,7 +25,7 @@ export const initialData: ActionLicenseState = {
isError: false,
};
-const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira';
+const MINIMUM_LICENSE_REQUIRED_CONNECTOR = ConnectorTypes.jira;
export const useGetActionLicense = (): ActionLicenseState => {
const [actionLicenseState, setActionLicensesState] = useState(initialData);
diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts
index 3df1891391c75..4f8713704361b 100644
--- a/x-pack/plugins/cases/server/client/cases/get.ts
+++ b/x-pack/plugins/cases/server/client/cases/get.ts
@@ -173,7 +173,6 @@ export const get = async (
let theCase: SavedObject;
let subCaseIds: string[] = [];
-
if (ENABLE_CASE_CONNECTOR) {
const [caseInfo, subCasesForCaseId] = await Promise.all([
caseService.getCase({
diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts
index d920c517a0004..f5a10d705e095 100644
--- a/x-pack/plugins/cases/server/client/cases/utils.ts
+++ b/x-pack/plugins/cases/server/client/cases/utils.ts
@@ -252,6 +252,7 @@ export const prepareFieldsForTransformation = ({
mappings.reduce(
(acc: PipedField[], mapping) =>
mapping != null &&
+ mapping.target != null &&
mapping.target !== 'not_mapped' &&
mapping.action_type !== 'nothing' &&
mapping.source !== 'comments'
diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts
index 7b8f57bf0d3bf..51c45bd25444e 100644
--- a/x-pack/plugins/cases/server/connectors/case/index.test.ts
+++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts
@@ -60,7 +60,7 @@ describe('case connector', () => {
connector: {
id: 'jira',
name: 'Jira',
- type: '.jira',
+ type: ConnectorTypes.jira,
fields: {
issueType: '10006',
priority: 'High',
@@ -99,7 +99,7 @@ describe('case connector', () => {
connector: {
id: 'jira',
name: 'Jira',
- type: '.jira',
+ type: ConnectorTypes.jira,
fields: {
issueType: '10006',
priority: 'High',
@@ -293,7 +293,7 @@ describe('case connector', () => {
connector: {
id: 'jira',
name: 'Jira',
- type: '.jira',
+ type: ConnectorTypes.jira,
fields: {
priority: 'High',
parent: null,
@@ -438,7 +438,7 @@ describe('case connector', () => {
connector: {
id: 'jira',
name: 'Jira',
- type: '.jira',
+ type: ConnectorTypes.jira,
fields: {
issueType: '10006',
priority: 'High',
@@ -640,7 +640,7 @@ describe('case connector', () => {
connector: {
id: 'jira',
name: 'Jira',
- type: '.jira',
+ type: ConnectorTypes.jira,
fields: {
priority: 'High',
parent: null,
@@ -974,7 +974,7 @@ describe('case connector', () => {
connector: {
id: 'jira',
name: 'Jira',
- type: '.jira',
+ type: ConnectorTypes.jira,
fields: {
issueType: '10006',
priority: 'High',
@@ -1003,7 +1003,7 @@ describe('case connector', () => {
connector: {
id: 'jira',
name: 'Jira',
- type: '.jira',
+ type: ConnectorTypes.jira,
fields: {
issueType: '10006',
priority: 'High',
diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts
index 596a5a4aae45e..79d3bf62e8a9e 100644
--- a/x-pack/plugins/cases/server/connectors/case/schema.ts
+++ b/x-pack/plugins/cases/server/connectors/case/schema.ts
@@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { CommentType } from '../../../common';
+import { CommentType, ConnectorTypes } from '../../../common';
import { validateConnector } from './validators';
// Reserved for future implementation
@@ -77,23 +77,29 @@ const ServiceNowSIRFieldsSchema = schema.object({
subcategory: schema.nullable(schema.string()),
});
+const SwimlaneFieldsSchema = schema.object({
+ caseId: schema.nullable(schema.string()),
+});
+
const NoneFieldsSchema = schema.nullable(schema.object({}));
const ReducedConnectorFieldsSchema: { [x: string]: any } = {
- '.jira': JiraFieldsSchema,
- '.resilient': ResilientFieldsSchema,
- '.servicenow-sir': ServiceNowSIRFieldsSchema,
+ [ConnectorTypes.jira]: JiraFieldsSchema,
+ [ConnectorTypes.resilient]: ResilientFieldsSchema,
+ [ConnectorTypes.serviceNowSIR]: ServiceNowSIRFieldsSchema,
+ [ConnectorTypes.swimlane]: SwimlaneFieldsSchema,
};
export const ConnectorProps = {
id: schema.string(),
name: schema.string(),
type: schema.oneOf([
- schema.literal('.servicenow'),
- schema.literal('.jira'),
- schema.literal('.resilient'),
- schema.literal('.servicenow-sir'),
- schema.literal('.none'),
+ schema.literal(ConnectorTypes.jira),
+ schema.literal(ConnectorTypes.none),
+ schema.literal(ConnectorTypes.resilient),
+ schema.literal(ConnectorTypes.serviceNowITSM),
+ schema.literal(ConnectorTypes.serviceNowSIR),
+ schema.literal(ConnectorTypes.swimlane),
]),
// Chain of conditional schemes
fields: Object.keys(ReducedConnectorFieldsSchema).reduce(
@@ -106,7 +112,7 @@ export const ConnectorProps = {
),
schema.conditional(
schema.siblingRef('type'),
- '.servicenow',
+ ConnectorTypes.serviceNowITSM,
ServiceNowITSMFieldsSchema,
NoneFieldsSchema
)
diff --git a/x-pack/plugins/cases/server/connectors/case/validators.ts b/x-pack/plugins/cases/server/connectors/case/validators.ts
index 03110d15c9d3f..6ab4f3a21a24f 100644
--- a/x-pack/plugins/cases/server/connectors/case/validators.ts
+++ b/x-pack/plugins/cases/server/connectors/case/validators.ts
@@ -6,9 +6,10 @@
*/
import { Connector } from './types';
+import { ConnectorTypes } from '../../../common';
export const validateConnector = (connector: Connector) => {
- if (connector.type === '.none' && connector.fields !== null) {
+ if (connector.type === ConnectorTypes.none && connector.fields !== null) {
return 'Fields must be set to null for connectors of type .none';
}
};
diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts
index 5ed7eb4ade4ca..d0ae7154fe5d9 100644
--- a/x-pack/plugins/cases/server/connectors/factory.ts
+++ b/x-pack/plugins/cases/server/connectors/factory.ts
@@ -6,16 +6,18 @@
*/
import { ConnectorTypes } from '../../common';
+import { ICasesConnector, CasesConnectorsMap } from './types';
import { getCaseConnector as getJiraCaseConnector } from './jira';
import { getCaseConnector as getResilientCaseConnector } from './resilient';
import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow';
-import { ICasesConnector, CasesConnectorsMap } from './types';
+import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane';
const mapping: Record = {
[ConnectorTypes.jira]: getJiraCaseConnector(),
[ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(),
[ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(),
[ConnectorTypes.resilient]: getResilientCaseConnector(),
+ [ConnectorTypes.swimlane]: getSwimlaneCaseConnector(),
[ConnectorTypes.none]: null,
};
diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts
new file mode 100644
index 0000000000000..55cbbdb68691e
--- /dev/null
+++ b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CaseResponse } from '../../../common';
+import { format } from './format';
+
+describe('Swimlane formatter', () => {
+ const theCase = {
+ id: 'case-id',
+ connector: { fields: null },
+ } as CaseResponse;
+
+ it('it formats correctly', async () => {
+ const res = await format(theCase, []);
+ expect(res).toEqual({ caseId: theCase.id });
+ });
+});
diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.ts
new file mode 100644
index 0000000000000..9531e4099a4f4
--- /dev/null
+++ b/x-pack/plugins/cases/server/connectors/swimlane/format.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ConnectorSwimlaneTypeFields } from '../../../common';
+import { Format } from './types';
+
+export const format: Format = (theCase) => {
+ const { caseId = theCase.id } =
+ (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {};
+ return { caseId };
+};
diff --git a/x-pack/plugins/cases/server/connectors/swimlane/index.ts b/x-pack/plugins/cases/server/connectors/swimlane/index.ts
new file mode 100644
index 0000000000000..2cad92391bdec
--- /dev/null
+++ b/x-pack/plugins/cases/server/connectors/swimlane/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getMapping } from './mapping';
+import { format } from './format';
+import { SwimlaneCaseConnector } from './types';
+
+export const getCaseConnector = (): SwimlaneCaseConnector => ({
+ getMapping,
+ format,
+});
diff --git a/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts b/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts
new file mode 100644
index 0000000000000..e1e34054463e5
--- /dev/null
+++ b/x-pack/plugins/cases/server/connectors/swimlane/mapping.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { GetMapping } from './types';
+
+export const getMapping: GetMapping = () => {
+ return [
+ {
+ source: 'title',
+ target: 'caseName',
+ action_type: 'overwrite',
+ },
+ {
+ source: 'description',
+ target: 'description',
+ action_type: 'overwrite',
+ },
+ {
+ source: 'comments',
+ target: 'comments',
+ action_type: 'append',
+ },
+ ];
+};
diff --git a/x-pack/plugins/cases/server/connectors/swimlane/types.ts b/x-pack/plugins/cases/server/connectors/swimlane/types.ts
new file mode 100644
index 0000000000000..22a1e9f6372d5
--- /dev/null
+++ b/x-pack/plugins/cases/server/connectors/swimlane/types.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SwimlaneFieldsType } from '../../../common/api';
+import { ICasesConnector } from '../types';
+
+export type SwimlaneCaseConnector = ICasesConnector;
+export type Format = ICasesConnector['format'];
+export type GetMapping = ICasesConnector['getMapping'];
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx
index 86f5564a17d52..59da0f0f4d17e 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx
@@ -27,6 +27,7 @@ describe('Background Search Session management status labels', () => {
id: 'wtywp9u2802hahgp-gsla',
restoreUrl: '/app/great-app-url/#45',
reloadUrl: '/app/great-app-url/#45',
+ numSearches: 1,
appId: 'security',
status: SearchSessionStatus.IN_PROGRESS,
created: '2020-12-02T00:19:32Z',
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx
index 42ff270ed44a0..6dfe3a5153670 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx
@@ -70,6 +70,7 @@ describe('Background Search Session Management Table', () => {
status: SearchSessionStatus.IN_PROGRESS,
created: '2020-12-02T00:19:32Z',
expires: '2020-12-07T00:19:32Z',
+ idMapping: {},
},
},
],
@@ -95,10 +96,12 @@ describe('Background Search Session Management Table', () => {
);
});
- expect(table.find('thead th').map((node) => node.text())).toMatchInlineSnapshot(`
+ expect(table.find('thead th .euiTableCellContent__text').map((node) => node.text()))
+ .toMatchInlineSnapshot(`
Array [
"App",
"Name",
+ "# Searches",
"Status",
"Created",
"Expiration",
@@ -130,6 +133,7 @@ describe('Background Search Session Management Table', () => {
Array [
"App",
"Namevery background search ",
+ "# Searches0",
"StatusExpired",
"Created2 Dec, 2020, 00:19:32",
"Expiration--",
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts
index 3857b08ad0a3a..cc79f8002a98c 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts
@@ -52,6 +52,7 @@ describe('Search Sessions Management API', () => {
status: 'complete',
initialState: {},
restoreState: {},
+ idMapping: [],
},
},
],
@@ -78,6 +79,7 @@ describe('Search Sessions Management API', () => {
"id": "hello-pizza-123",
"initialState": Object {},
"name": "Veggie",
+ "numSearches": 0,
"reloadUrl": "hello-cool-undefined-url",
"restoreState": Object {},
"restoreUrl": "hello-cool-undefined-url",
@@ -100,6 +102,7 @@ describe('Search Sessions Management API', () => {
expires: moment().subtract(3, 'days'),
initialState: {},
restoreState: {},
+ idMapping: {},
},
},
],
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
index 3710dfa16e76b..0369dc4a839b5 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
@@ -90,6 +90,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema)
urlGeneratorId,
initialState,
restoreState,
+ idMapping,
} = savedObject.attributes;
const status = getUIStatus(savedObject.attributes);
@@ -113,6 +114,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema)
reloadUrl,
initialState,
restoreState,
+ numSearches: Object.keys(idMapping).length,
};
};
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx
index 4b68e0c9e2afd..fc4e67360ea4a 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx
@@ -70,6 +70,7 @@ describe('Search Sessions Management table column factory', () => {
reloadUrl: '/app/great-app-url',
restoreUrl: '/app/great-app-url/#42',
appId: 'discovery',
+ numSearches: 3,
status: SearchSessionStatus.IN_PROGRESS,
created: '2020-12-02T00:19:32Z',
expires: '2020-12-07T00:19:32Z',
@@ -95,6 +96,12 @@ describe('Search Sessions Management table column factory', () => {
"sortable": true,
"width": "20%",
},
+ Object {
+ "field": "numSearches",
+ "name": "# Searches",
+ "render": [Function],
+ "sortable": true,
+ },
Object {
"field": "status",
"name": "Status",
@@ -146,10 +153,29 @@ describe('Search Sessions Management table column factory', () => {
});
});
+ // Num of searches column
+ describe('num of searches', () => {
+ test('renders', () => {
+ const [, , numOfSearches] = getColumns(
+ mockCoreStart,
+ mockPluginsSetup,
+ api,
+ mockConfig,
+ tz,
+ handleAction
+ ) as Array>;
+
+ const numOfSearchesLine = mount(
+ numOfSearches.render!(mockSession.numSearches, mockSession) as ReactElement
+ );
+ expect(numOfSearchesLine.text()).toMatchInlineSnapshot(`"3"`);
+ });
+ });
+
// Status column
describe('status', () => {
test('render in_progress', () => {
- const [, , status] = getColumns(
+ const [, , , status] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@@ -165,7 +191,7 @@ describe('Search Sessions Management table column factory', () => {
});
test('error handling', () => {
- const [, , status] = getColumns(
+ const [, , , status] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@@ -188,7 +214,7 @@ describe('Search Sessions Management table column factory', () => {
test('render using Browser timezone', () => {
tz = 'Browser';
- const [, , , createdDateCol] = getColumns(
+ const [, , , , createdDateCol] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@@ -205,7 +231,7 @@ describe('Search Sessions Management table column factory', () => {
test('render using AK timezone', () => {
tz = 'US/Alaska';
- const [, , , createdDateCol] = getColumns(
+ const [, , , , createdDateCol] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@@ -220,7 +246,7 @@ describe('Search Sessions Management table column factory', () => {
});
test('error handling', () => {
- const [, , , createdDateCol] = getColumns(
+ const [, , , , createdDateCol] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx
index 1805ef52b85f1..d8d2fa0aeac59 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx
@@ -120,6 +120,20 @@ export const getColumns = (
},
},
+ // # Searches
+ {
+ field: 'numSearches',
+ name: i18n.translate('xpack.data.mgmt.searchSessions.table.numSearches', {
+ defaultMessage: '# Searches',
+ }),
+ sortable: true,
+ render: (numSearches: UISession['numSearches'], session) => (
+
+ {numSearches}
+
+ ),
+ },
+
// Session status
{
field: 'status',
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts
index d0d5ee9fb17dd..6a8ace8dbdc79 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts
@@ -34,6 +34,7 @@ export interface UISession {
created: string;
expires: string | null;
status: UISearchSessionState;
+ numSearches: number;
actions?: ACTION[];
reloadUrl: string;
restoreUrl: string;
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
index 138f42549a094..81a12f607935d 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
@@ -24,7 +24,11 @@ import {
ENHANCED_ES_SEARCH_STRATEGY,
SEARCH_SESSION_TYPE,
} from '../../../../../../src/plugins/data/common';
-import { esKuery, ISearchSessionService } from '../../../../../../src/plugins/data/server';
+import {
+ esKuery,
+ ISearchSessionService,
+ NoSearchIdInSessionError,
+} from '../../../../../../src/plugins/data/server';
import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server';
import {
TaskManagerSetupContract,
@@ -436,7 +440,7 @@ export class SearchSessionService
const requestHash = createRequestHash(searchRequest.params);
if (!session.attributes.idMapping.hasOwnProperty(requestHash)) {
this.logger.error(`getId | ${sessionId} | ${requestHash} not found`);
- throw new Error('No search ID in this session matching the given search request');
+ throw new NoSearchIdInSessionError();
}
this.logger.debug(`getId | ${sessionId} | ${requestHash}`);
diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json
index b024a52e64721..00eb3d7bf142c 100644
--- a/x-pack/plugins/data_visualizer/kibana.json
+++ b/x-pack/plugins/data_visualizer/kibana.json
@@ -16,7 +16,8 @@
"security",
"maps",
"home",
- "lens"
+ "lens",
+ "indexPatternFieldEditor"
],
"requiredBundles": [
"home",
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx
index f6f53f40d6b9e..52ae5e685316d 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx
@@ -18,7 +18,7 @@ import {
import { useUrlState } from '../../util/url_state';
import { useDataVisualizerKibana } from '../../../kibana_context';
-import { dataVisualizerTimefilterRefresh$ } from '../../../index_data_visualizer/services/timefilter_refresh_service';
+import { dataVisualizerRefresh$ } from '../../../index_data_visualizer/services/timefilter_refresh_service';
interface TimePickerQuickRange {
from: string;
@@ -50,7 +50,7 @@ function getRecentlyUsedRangesFactory(timeHistory: TimeHistoryContract) {
}
function updateLastRefresh(timeRange: OnRefreshProps) {
- dataVisualizerTimefilterRefresh$.next({ lastRefresh: Date.now(), timeRange });
+ dataVisualizerRefresh$.next({ lastRefresh: Date.now(), timeRange });
}
export const DatePickerWrapper: FC = () => {
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts
index 414c72c33f057..a77ca1d589349 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts
@@ -7,19 +7,37 @@
import { i18n } from '@kbn/i18n';
import { Action } from '@elastic/eui/src/components/basic_table/action_types';
+import { MutableRefObject } from 'react';
import { getCompatibleLensDataType, getLensAttributes } from './lens_utils';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { FieldVisConfig } from '../../stats_table/types';
-import { LensPublicStart } from '../../../../../../../lens/public';
+import { DataVisualizerKibanaReactContextValue } from '../../../../kibana_context';
+import {
+ dataVisualizerRefresh$,
+ Refresh,
+} from '../../../../index_data_visualizer/services/timefilter_refresh_service';
+
export function getActions(
indexPattern: IndexPattern,
- lensPlugin: LensPublicStart,
- combinedQuery: CombinedQuery
+ services: DataVisualizerKibanaReactContextValue['services'],
+ combinedQuery: CombinedQuery,
+ actionFlyoutRef: MutableRefObject<(() => void | undefined) | undefined>
): Array> {
- const canUseLensEditor = lensPlugin.canUseEditor();
- return [
- {
+ const { lens: lensPlugin, indexPatternFieldEditor } = services;
+
+ const actions: Array> = [];
+
+ const refreshPage = () => {
+ const refresh: Refresh = {
+ lastRefresh: Date.now(),
+ };
+ dataVisualizerRefresh$.next(refresh);
+ };
+ // Navigate to Lens with prefilled chart for data field
+ if (lensPlugin !== undefined) {
+ const canUseLensEditor = lensPlugin?.canUseEditor();
+ actions.push({
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.exploreInLensTitle', {
defaultMessage: 'Explore in Lens',
}),
@@ -40,6 +58,56 @@ export function getActions(
}
},
'data-test-subj': 'dataVisualizerActionViewInLensButton',
- },
- ];
+ });
+ }
+
+ // Allow to edit index pattern field
+ if (indexPatternFieldEditor?.userPermissions.editIndexPattern()) {
+ actions.push({
+ name: i18n.translate('xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldTitle', {
+ defaultMessage: 'Edit index pattern field',
+ }),
+ description: i18n.translate(
+ 'xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldDescription',
+ {
+ defaultMessage: 'Edit index pattern field',
+ }
+ ),
+ type: 'icon',
+ icon: 'indexEdit',
+ onClick: (item: FieldVisConfig) => {
+ actionFlyoutRef.current = indexPatternFieldEditor?.openEditor({
+ ctx: { indexPattern },
+ fieldName: item.fieldName,
+ onSave: refreshPage,
+ });
+ },
+ 'data-test-subj': 'dataVisualizerActionEditIndexPatternFieldButton',
+ });
+ actions.push({
+ name: i18n.translate('xpack.dataVisualizer.index.dataGrid.deleteIndexPatternFieldTitle', {
+ defaultMessage: 'Delete index pattern field',
+ }),
+ description: i18n.translate(
+ 'xpack.dataVisualizer.index.dataGrid.deleteIndexPatternFieldDescription',
+ {
+ defaultMessage: 'Delete index pattern field',
+ }
+ ),
+ type: 'icon',
+ icon: 'trash',
+ available: (item: FieldVisConfig) => {
+ return item.deletable === true;
+ },
+ onClick: (item: FieldVisConfig) => {
+ actionFlyoutRef.current = indexPatternFieldEditor?.openDeleteModal({
+ ctx: { indexPattern },
+ fieldName: item.fieldName!,
+ onDelete: refreshPage,
+ });
+ },
+ 'data-test-subj': 'dataVisualizerActionDeleteIndexPatternFieldButton',
+ });
+ }
+ return actions;
}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx
index 238cdcc2f8d9e..6c9df5cf2eba7 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx
@@ -64,7 +64,7 @@ export const FilebeatConfigFlyout: FC = ({
}, [username, index, ingestPipelineId, results]);
return (
-
+
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx
index 22fe8244ef760..1baea4b3f2f7c 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx
@@ -14,13 +14,6 @@ import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
import { ChoroplethMap } from './choropleth_map';
-const COMMON_EMS_LAYER_IDS = [
- 'world_countries',
- 'administrative_regions_lvl2',
- 'usa_zip_codes',
- 'usa_states',
-];
-
export const KeywordContent: FC = ({ config }) => {
const [EMSSuggestion, setEMSSuggestion] = useState();
const { stats, fieldName } = config;
@@ -32,7 +25,6 @@ export const KeywordContent: FC = ({ config }) => {
const loadEMSTermSuggestions = useCallback(async () => {
if (!mapsPlugin) return;
const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({
- emsLayerIds: COMMON_EMS_LAYER_IDS,
sampleValues: Array.isArray(stats?.topValues)
? stats?.topValues.map((value) => value.key)
: [],
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
index afadc5c5ae4a4..02e4e29dcc05e 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
@@ -15,6 +15,7 @@ import {
EuiIcon,
EuiInMemoryTable,
EuiText,
+ EuiToolTip,
HorizontalAlignment,
LEFT_ALIGNMENT,
RIGHT_ALIGNMENT,
@@ -111,6 +112,7 @@ export const DataVisualizerTable = ({
width: '40px',
isExpander: true,
render: (item: DataVisualizerTableItem) => {
+ const displayName = item.displayName ?? item.fieldName;
if (item.fieldName === undefined) return null;
const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown';
return (
@@ -121,11 +123,11 @@ export const DataVisualizerTable = ({
expandedRowItemIds.includes(item.fieldName)
? i18n.translate('xpack.dataVisualizer.dataGrid.rowCollapse', {
defaultMessage: 'Hide details for {fieldName}',
- values: { fieldName: item.fieldName },
+ values: { fieldName: displayName },
})
: i18n.translate('xpack.dataVisualizer.dataGrid.rowExpand', {
defaultMessage: 'Show details for {fieldName}',
- values: { fieldName: item.fieldName },
+ values: { fieldName: displayName },
})
}
iconType={direction}
@@ -157,11 +159,15 @@ export const DataVisualizerTable = ({
}),
sortable: true,
truncateText: true,
- render: (fieldName: string) => (
-
- {fieldName}
-
- ),
+ render: (fieldName: string, item: DataVisualizerTableItem) => {
+ const displayName = item.displayName ?? item.fieldName;
+
+ return (
+
+ {displayName}
+
+ );
+ },
align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnName',
},
@@ -194,18 +200,33 @@ export const DataVisualizerTable = ({
{i18n.translate('xpack.dataVisualizer.dataGrid.distributionsColumnName', {
defaultMessage: 'Distributions',
})}
- toggleShowDistribution()}
- aria-label={i18n.translate(
- 'xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel',
- {
- defaultMessage: 'Show distributions',
+
+ toggleShowDistribution()}
+ aria-label={
+ !showDistributions
+ ? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel', {
+ defaultMessage: 'Show distributions',
+ })
+ : i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsAriaLabel', {
+ defaultMessage: 'Hide distributions',
+ })
}
- )}
- />
+ />
+
),
render: (item: DataVisualizerTableItem) => {
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts
index d58497f6cd7cc..eeb9fe12692fd 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts
@@ -24,17 +24,20 @@ export interface MetricFieldVisStats {
export interface FieldVisConfig {
type: JobFieldType;
fieldName?: string;
+ displayName?: string;
existsInDocs: boolean;
aggregatable: boolean;
loading: boolean;
stats?: FieldVisStats;
fieldFormat?: any;
isUnsupportedType?: boolean;
+ deletable?: boolean;
}
export interface FileBasedFieldVisConfig {
type: JobFieldType;
fieldName?: string;
+ displayName?: string;
stats?: FieldVisStats;
format?: string;
}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
index 12441bcfbbb23..b116b25670ad2 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { FC, Fragment, useEffect, useMemo, useState, useCallback } from 'react';
+import React, { FC, Fragment, useEffect, useMemo, useState, useCallback, useRef } from 'react';
import { merge } from 'rxjs';
import {
EuiFlexGroup,
@@ -62,10 +62,11 @@ import { kbnTypeToJobType } from '../../../common/util/field_types_utils';
import { SearchPanel } from '../search_panel';
import { ActionsPanel } from '../actions_panel';
import { DatePickerWrapper } from '../../../common/components/date_picker_wrapper';
-import { dataVisualizerTimefilterRefresh$ } from '../../services/timefilter_refresh_service';
+import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service';
import { HelpMenu } from '../../../common/components/help_menu';
import { TimeBuckets } from '../../services/time_buckets';
import { extractSearchData } from '../../utils/saved_search_utils';
+import { DataVisualizerIndexPatternManagement } from '../index_pattern_management';
interface DataVisualizerPageState {
overallStats: OverallStats;
@@ -123,9 +124,8 @@ export interface IndexDataVisualizerViewProps {
const restorableDefaults = getDefaultDataVisualizerListState();
export const IndexDataVisualizerView: FC = (dataVisualizerProps) => {
- const {
- services: { lens: lensPlugin, docLinks, notifications, uiSettings },
- } = useDataVisualizerKibana();
+ const { services } = useDataVisualizerKibana();
+ const { docLinks, notifications, uiSettings } = services;
const { toasts } = notifications;
const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState(
@@ -299,7 +299,7 @@ export const IndexDataVisualizerView: FC = (dataVi
useEffect(() => {
const timeUpdateSubscription = merge(
timefilter.getTimeUpdate$(),
- dataVisualizerTimefilterRefresh$
+ dataVisualizerRefresh$
).subscribe(() => {
setGlobalState({
time: timefilter.getTime(),
@@ -533,7 +533,7 @@ export const IndexDataVisualizerView: FC = (dataVi
});
const metricExistsFields = allMetricFields.filter((f) => {
return aggregatableExistsFields.find((existsF) => {
- return existsF.fieldName === f.displayName;
+ return existsF.fieldName === f.spec.name;
});
});
@@ -562,7 +562,7 @@ export const IndexDataVisualizerView: FC = (dataVi
metricFieldsToShow.forEach((field) => {
const fieldData = aggregatableFields.find((f) => {
- return f.fieldName === field.displayName;
+ return f.fieldName === field.spec.name;
});
const metricConfig: FieldVisConfig = {
@@ -571,7 +571,11 @@ export const IndexDataVisualizerView: FC = (dataVi
type: JOB_FIELD_TYPES.NUMBER,
loading: true,
aggregatable: true,
+ deletable: field.runtimeField !== undefined,
};
+ if (field.displayName !== metricConfig.fieldName) {
+ metricConfig.displayName = field.displayName;
+ }
configs.push(metricConfig);
});
@@ -607,7 +611,7 @@ export const IndexDataVisualizerView: FC = (dataVi
allNonMetricFields.forEach((f) => {
const checkAggregatableField = aggregatableExistsFields.find(
- (existsField) => existsField.fieldName === f.displayName
+ (existsField) => existsField.fieldName === f.spec.name
);
if (checkAggregatableField !== undefined) {
@@ -615,7 +619,7 @@ export const IndexDataVisualizerView: FC = (dataVi
nonMetricFieldData.push(checkAggregatableField);
} else {
const checkNonAggregatableField = nonAggregatableExistsFields.find(
- (existsField) => existsField.fieldName === f.displayName
+ (existsField) => existsField.fieldName === f.spec.name
);
if (checkNonAggregatableField !== undefined) {
@@ -643,7 +647,7 @@ export const IndexDataVisualizerView: FC = (dataVi
const configs: FieldVisConfig[] = [];
nonMetricFieldsToShow.forEach((field) => {
- const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.displayName);
+ const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
const nonMetricConfig = {
...fieldData,
@@ -651,6 +655,7 @@ export const IndexDataVisualizerView: FC = (dataVi
aggregatable: field.aggregatable,
scripted: field.scripted,
loading: fieldData.existsInDocs,
+ deletable: field.runtimeField !== undefined,
};
// Map the field type from the Kibana index pattern to the field type
@@ -665,6 +670,10 @@ export const IndexDataVisualizerView: FC = (dataVi
nonMetricConfig.isUnsupportedType = true;
}
+ if (field.displayName !== nonMetricConfig.fieldName) {
+ nonMetricConfig.displayName = field.displayName;
+ }
+
configs.push(nonMetricConfig);
});
@@ -735,13 +744,33 @@ export const IndexDataVisualizerView: FC = (dataVi
[currentIndexPattern, searchQueryLanguage, searchString]
);
+ // Some actions open up fly-out or popup
+ // This variable is used to keep track of them and clean up when unmounting
+ const actionFlyoutRef = useRef<() => void | undefined>();
+ useEffect(() => {
+ const ref = actionFlyoutRef;
+ return () => {
+ // Clean up any of the flyout/editor opened from the actions
+ if (ref.current) {
+ ref.current();
+ }
+ };
+ }, []);
+
// Inject custom action column for the index based visualizer
+ // Hide the column completely if no access to any of the plugins
const extendedColumns = useMemo(() => {
- if (lensPlugin === undefined) {
- // eslint-disable-next-line no-console
- console.error('Lens plugin not available');
- return;
- }
+ const actions = getActions(
+ currentIndexPattern,
+ services,
+ {
+ searchQueryLanguage,
+ searchString,
+ },
+ actionFlyoutRef
+ );
+ if (!Array.isArray(actions) || actions.length < 1) return;
+
const actionColumn: EuiTableActionsColumnType = {
name: (
= (dataVi
defaultMessage="Actions"
/>
),
- actions: getActions(currentIndexPattern, lensPlugin, { searchQueryLanguage, searchString }),
+ actions,
width: '100px',
};
return [actionColumn];
- }, [currentIndexPattern, lensPlugin, searchQueryLanguage, searchString]);
+ }, [currentIndexPattern, services, searchQueryLanguage, searchString]);
const helpLink = docLinks.links.ml.guide;
+
return (
@@ -765,10 +795,24 @@ export const IndexDataVisualizerView: FC = (dataVi
-
- {currentIndexPattern.title}
-
+
+
+ {currentIndexPattern.title}
+
+
+
+
{currentIndexPattern.timeFieldName !== undefined && (
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index.ts
new file mode 100644
index 0000000000000..c26f84a4c22fc
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { DataVisualizerIndexPatternManagement } from './index_pattern_management';
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index_pattern_management.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index_pattern_management.tsx
new file mode 100644
index 0000000000000..cb81640f328c5
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index_pattern_management.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useRef, useState } from 'react';
+import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { IndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
+import { useDataVisualizerKibana } from '../../../kibana_context';
+import { dataVisualizerRefresh$, Refresh } from '../../services/timefilter_refresh_service';
+
+export interface DataVisualizerIndexPatternManagementProps {
+ /**
+ * Currently selected index pattern
+ */
+ currentIndexPattern?: IndexPattern;
+ /**
+ * Read from the Fields API
+ */
+ useNewFieldsApi?: boolean;
+}
+
+export function DataVisualizerIndexPatternManagement(
+ props: DataVisualizerIndexPatternManagementProps
+) {
+ const {
+ services: { indexPatternFieldEditor, application },
+ } = useDataVisualizerKibana();
+
+ const { useNewFieldsApi, currentIndexPattern } = props;
+ const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern();
+ const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi;
+ const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false);
+
+ const closeFieldEditor = useRef<() => void | undefined>();
+ useEffect(() => {
+ return () => {
+ // Make sure to close the editor when unmounting
+ if (closeFieldEditor.current) {
+ closeFieldEditor.current();
+ }
+ };
+ }, []);
+
+ if (indexPatternFieldEditor === undefined || !currentIndexPattern || !canEditIndexPatternField) {
+ return null;
+ }
+
+ const addField = () => {
+ closeFieldEditor.current = indexPatternFieldEditor.openEditor({
+ ctx: {
+ indexPattern: currentIndexPattern,
+ },
+ onSave: () => {
+ const refresh: Refresh = {
+ lastRefresh: Date.now(),
+ };
+ dataVisualizerRefresh$.next(refresh);
+ },
+ });
+ };
+
+ return (
+ {
+ setIsAddIndexPatternFieldPopoverOpen(false);
+ }}
+ ownFocus
+ data-test-subj="dataVisualizerIndexPatternManagementPopover"
+ button={
+ {
+ setIsAddIndexPatternFieldPopoverOpen(!isAddIndexPatternFieldPopoverOpen);
+ }}
+ />
+ }
+ >
+ {
+ setIsAddIndexPatternFieldPopoverOpen(false);
+ addField();
+ }}
+ >
+ {i18n.translate('xpack.dataVisualizer.index.indexPatternManagement.addFieldButton', {
+ defaultMessage: 'Add field to index pattern',
+ })}
+ ,
+ {
+ setIsAddIndexPatternFieldPopoverOpen(false);
+ application.navigateToApp('management', {
+ path: `/kibana/indexPatterns/patterns/${props.currentIndexPattern?.id}`,
+ });
+ }}
+ >
+ {i18n.translate('xpack.dataVisualizer.index.indexPatternManagement.manageFieldButton', {
+ defaultMessage: 'Manage index pattern fields',
+ })}
+ ,
+ ]}
+ />
+
+ );
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
index 3cb0d4d672f48..468bd3a2bd7ee 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
@@ -50,9 +50,9 @@ export class DataLoader {
const fieldName = field.displayName !== undefined ? field.displayName : field.name;
if (this.isDisplayField(fieldName) === true) {
if (field.aggregatable === true && field.type !== KBN_FIELD_TYPES.GEO_SHAPE) {
- aggregatableFields.push(fieldName);
+ aggregatableFields.push(field.name);
} else {
- nonAggregatableFields.push(fieldName);
+ nonAggregatableFields.push(field.name);
}
}
});
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
index 82a9b93b31a71..f9e9aece48a06 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
@@ -178,7 +178,16 @@ export const DataVisualizerUrlStateContextProvider: FC {
const coreStart = getCoreStart();
- const { data, maps, embeddable, share, security, fileUpload, lens } = getPluginsStart();
+ const {
+ data,
+ maps,
+ embeddable,
+ share,
+ security,
+ fileUpload,
+ lens,
+ indexPatternFieldEditor,
+ } = getPluginsStart();
const services = {
data,
maps,
@@ -187,6 +196,7 @@ export const IndexDataVisualizer: FC = () => {
security,
fileUpload,
lens,
+ indexPatternFieldEditor,
...coreStart,
};
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts
index 49ef9107c3ece..11f286e781219 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts
@@ -6,11 +6,10 @@
*/
import { Subject } from 'rxjs';
-import { Required } from 'utility-types';
export interface Refresh {
lastRefresh: number;
timeRange?: { start: string; end: string };
}
-export const dataVisualizerTimefilterRefresh$ = new Subject>();
+export const dataVisualizerRefresh$ = new Subject();
diff --git a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts
index f7ce13d2fd48d..58d0ac021ff22 100644
--- a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts
+++ b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts
@@ -6,8 +6,9 @@
*/
import { CoreStart } from 'kibana/public';
-import { useKibana } from '../../../../../src/plugins/kibana_react/public';
+import { KibanaReactContextValue, useKibana } from '../../../../../src/plugins/kibana_react/public';
import type { DataVisualizerStartDependencies } from '../plugin';
export type StartServices = CoreStart & DataVisualizerStartDependencies;
+export type DataVisualizerKibanaReactContextValue = KibanaReactContextValue;
export const useDataVisualizerKibana = () => useKibana();
diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts
index 66109de1b1463..4b71b08e9cf27 100644
--- a/x-pack/plugins/data_visualizer/public/plugin.ts
+++ b/x-pack/plugins/data_visualizer/public/plugin.ts
@@ -17,6 +17,7 @@ import type { FileUploadPluginStart } from '../../file_upload/public';
import type { MapsStartApi } from '../../maps/public';
import type { SecurityPluginSetup } from '../../security/public';
import type { LensPublicStart } from '../../lens/public';
+import type { IndexPatternFieldEditorStart } from '../../../../src/plugins/index_pattern_field_editor/public';
import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api';
import { getMaxBytesFormatted } from './application/common/util/get_max_bytes';
import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home';
@@ -32,6 +33,7 @@ export interface DataVisualizerStartDependencies {
security?: SecurityPluginSetup;
share: SharePluginStart;
lens?: LensPublicStart;
+ indexPatternFieldEditor?: IndexPatternFieldEditorStart;
}
export type DataVisualizerPluginSetup = ReturnType;
diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json
index 01a3624d3e320..da95a0f21a020 100644
--- a/x-pack/plugins/discover_enhanced/kibana.json
+++ b/x-pack/plugins/discover_enhanced/kibana.json
@@ -7,5 +7,5 @@
"requiredPlugins": ["uiActions", "embeddable", "discover"],
"optionalPlugins": ["share", "kibanaLegacy", "usageCollection"],
"configPath": ["xpack", "discoverEnhanced"],
- "requiredBundles": ["kibanaUtils", "data", "share"]
+ "requiredBundles": ["kibanaUtils", "data"]
}
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts
index 023db127ca633..44ea53fe0b870 100644
--- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts
@@ -11,13 +11,13 @@ import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/
import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public';
import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public';
import { CoreStart } from '../../../../../../src/core/public';
-import { KibanaURL } from '../../../../../../src/plugins/share/public';
+import { KibanaLocation } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA';
export interface PluginDeps {
- discover: Pick;
+ discover: Pick;
kibanaLegacy?: {
dashboardConfig: {
getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls'];
@@ -26,7 +26,7 @@ export interface PluginDeps {
}
export interface CoreDeps {
- application: Pick;
+ application: Pick;
}
export interface Params {
@@ -43,7 +43,7 @@ export abstract class AbstractExploreDataAction;
+ protected abstract getLocation(context: Context): Promise;
public async isCompatible({ embeddable }: Context): Promise {
if (!embeddable) return false;
@@ -52,7 +52,7 @@ export abstract class AbstractExploreDataAction {
- type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
-
const core = coreMock.createStart();
-
- const urlGenerator: UrlGenerator = ({
- createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
- } as unknown) as UrlGenerator;
+ const locator: DiscoverAppLocator = {
+ getLocation: jest.fn(() =>
+ Promise.resolve({
+ app: 'discover',
+ path: '/foo#bar',
+ state: {},
+ })
+ ),
+ navigate: jest.fn(async () => {}),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ };
const plugins: PluginDeps = {
discover: {
- urlGenerator,
+ locator,
},
kibanaLegacy: {
dashboardConfig: {
@@ -95,7 +101,7 @@ const setup = (
embeddable,
} as ExploreDataChartActionContext;
- return { core, plugins, urlGenerator, params, action, input, output, embeddable, context };
+ return { core, plugins, locator, params, action, input, output, embeddable, context };
};
describe('"Explore underlying data" panel action', () => {
@@ -132,7 +138,7 @@ describe('"Explore underlying data" panel action', () => {
test('returns false when URL generator is not present', async () => {
const { action, plugins, context } = setup();
- (plugins.discover as any).urlGenerator = undefined;
+ (plugins.discover as any).locator = undefined;
const isCompatible = await action.isCompatible(context);
@@ -205,23 +211,15 @@ describe('"Explore underlying data" panel action', () => {
});
describe('getHref()', () => {
- test('returns URL path generated by URL generator', async () => {
- const { action, context } = setup();
-
- const href = await action.getHref(context);
-
- expect(href).toBe('/xyz/app/discover/foo#bar');
- });
-
test('calls URL generator with right arguments', async () => {
- const { action, urlGenerator, context } = setup();
+ const { action, locator, context } = setup();
- expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0);
+ expect(locator.getLocation).toHaveBeenCalledTimes(0);
await action.getHref(context);
- expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1);
- expect(urlGenerator.createUrl).toHaveBeenCalledWith({
+ expect(locator.getLocation).toHaveBeenCalledTimes(1);
+ expect(locator.getLocation).toHaveBeenCalledWith({
filters: [],
indexPatternId: 'index-ptr-foo',
timeRange: undefined,
@@ -260,11 +258,11 @@ describe('"Explore underlying data" panel action', () => {
},
];
- const { action, context, urlGenerator } = setup({ filters, timeFieldName });
+ const { action, context, locator } = setup({ filters, timeFieldName });
await action.getHref(context);
- expect(urlGenerator.createUrl).toHaveBeenCalledWith({
+ expect(locator.getLocation).toHaveBeenCalledWith({
filters: [
{
meta: {
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts
index 32264ee1deceb..7b59a4e51d042 100644
--- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts
@@ -7,7 +7,7 @@
import { Action } from '../../../../../../src/plugins/ui_actions/public';
import {
- DiscoverUrlGeneratorState,
+ DiscoverAppLocatorParams,
SearchInput,
} from '../../../../../../src/plugins/discover/public';
import {
@@ -15,7 +15,7 @@ import {
esFilters,
} from '../../../../../../src/plugins/data/public';
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
-import { KibanaURL } from '../../../../../../src/plugins/share/public';
+import { KibanaLocation } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
import { AbstractExploreDataAction } from './abstract_explore_data_action';
@@ -43,14 +43,14 @@ export class ExploreDataChartAction
return super.isCompatible(context);
}
- protected readonly getUrl = async (
+ protected readonly getLocation = async (
context: ExploreDataChartActionContext
- ): Promise => {
+ ): Promise => {
const { plugins } = this.params.start();
- const { urlGenerator } = plugins.discover;
+ const { locator } = plugins.discover;
- if (!urlGenerator) {
- throw new Error('Discover URL generator not available.');
+ if (!locator) {
+ throw new Error('Discover URL locator not available.');
}
const { embeddable } = context;
@@ -59,23 +59,23 @@ export class ExploreDataChartAction
context.timeFieldName
);
- const state: DiscoverUrlGeneratorState = {
+ const params: DiscoverAppLocatorParams = {
filters,
timeRange,
};
if (embeddable) {
- state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
+ params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
const input = embeddable.getInput() as Readonly;
- if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange;
- if (input.query) state.query = input.query;
- if (input.filters) state.filters = [...input.filters, ...(state.filters || [])];
+ if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange;
+ if (input.query) params.query = input.query;
+ if (input.filters) params.filters = [...input.filters, ...(params.filters || [])];
}
- const path = await urlGenerator.createUrl(state);
+ const location = await locator.getLocation(params);
- return new KibanaURL(path);
+ return location;
};
}
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts
index 842c7d6b339b4..5bdac602ec271 100644
--- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts
@@ -8,13 +8,13 @@
import { ExploreDataContextMenuAction } from './explore_data_context_menu_action';
import { Params, PluginDeps } from './abstract_explore_data_action';
import { coreMock } from '../../../../../../src/core/public/mocks';
-import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public';
import { i18n } from '@kbn/i18n';
import {
VisualizeEmbeddableContract,
VISUALIZE_EMBEDDABLE_TYPE,
} from '../../../../../../src/plugins/visualizations/public';
import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
+import { DiscoverAppLocator } from '../../../../../../src/plugins/discover/public';
const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance;
@@ -29,17 +29,23 @@ afterEach(() => {
});
const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = {}) => {
- type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
-
const core = coreMock.createStart();
-
- const urlGenerator: UrlGenerator = ({
- createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')),
- } as unknown) as UrlGenerator;
+ const locator: DiscoverAppLocator = {
+ getLocation: jest.fn(() =>
+ Promise.resolve({
+ app: 'discover',
+ path: '/foo#bar',
+ state: {},
+ })
+ ),
+ navigate: jest.fn(async () => {}),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ };
const plugins: PluginDeps = {
discover: {
- urlGenerator,
+ locator,
},
kibanaLegacy: {
dashboardConfig: {
@@ -79,7 +85,7 @@ const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } =
embeddable,
};
- return { core, plugins, urlGenerator, params, action, input, output, embeddable, context };
+ return { core, plugins, locator, params, action, input, output, embeddable, context };
};
describe('"Explore underlying data" panel action', () => {
@@ -116,7 +122,7 @@ describe('"Explore underlying data" panel action', () => {
test('returns false when URL generator is not present', async () => {
const { action, plugins, context } = setup();
- (plugins.discover as any).urlGenerator = undefined;
+ (plugins.discover as any).locator = undefined;
const isCompatible = await action.isCompatible(context);
@@ -189,23 +195,15 @@ describe('"Explore underlying data" panel action', () => {
});
describe('getHref()', () => {
- test('returns URL path generated by URL generator', async () => {
- const { action, context } = setup();
-
- const href = await action.getHref(context);
-
- expect(href).toBe('/xyz/app/discover/foo#bar');
- });
-
test('calls URL generator with right arguments', async () => {
- const { action, urlGenerator, context } = setup();
+ const { action, locator, context } = setup();
- expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0);
+ expect(locator.getLocation).toHaveBeenCalledTimes(0);
await action.getHref(context);
- expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1);
- expect(urlGenerator.createUrl).toHaveBeenCalledWith({
+ expect(locator.getLocation).toHaveBeenCalledTimes(1);
+ expect(locator.getLocation).toHaveBeenCalledWith({
indexPatternId: 'index-ptr-foo',
});
});
diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts
index 99a2afd239645..88c093a299cb9 100644
--- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts
+++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts
@@ -12,8 +12,8 @@ import {
IEmbeddable,
} from '../../../../../../src/plugins/embeddable/public';
import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public';
-import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public';
-import { KibanaURL } from '../../../../../../src/plugins/share/public';
+import { DiscoverAppLocatorParams } from '../../../../../../src/plugins/discover/public';
+import { KibanaLocation } from '../../../../../../src/plugins/share/public';
import * as shared from './shared';
import { AbstractExploreDataAction } from './abstract_explore_data_action';
@@ -40,29 +40,31 @@ export class ExploreDataContextMenuAction
public readonly order = 200;
- protected readonly getUrl = async (context: EmbeddableQueryContext): Promise => {
+ protected readonly getLocation = async (
+ context: EmbeddableQueryContext
+ ): Promise => {
const { plugins } = this.params.start();
- const { urlGenerator } = plugins.discover;
+ const { locator } = plugins.discover;
- if (!urlGenerator) {
- throw new Error('Discover URL generator not available.');
+ if (!locator) {
+ throw new Error('Discover URL locator not available.');
}
const { embeddable } = context;
- const state: DiscoverUrlGeneratorState = {};
+ const params: DiscoverAppLocatorParams = {};
if (embeddable) {
- state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
+ params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined;
const input = embeddable.getInput();
- if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange;
- if (input.query) state.query = input.query;
- if (input.filters) state.filters = [...input.filters, ...(state.filters || [])];
+ if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange;
+ if (input.query) params.query = input.query;
+ if (input.filters) params.filters = [...input.filters, ...(params.filters || [])];
}
- const path = await urlGenerator.createUrl(state);
+ const location = await locator.getLocation(params);
- return new KibanaURL(path);
+ return location;
};
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx
index c2a11ec06fa6a..5b082ce8d26ba 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx
@@ -13,10 +13,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
-import { rerender } from '../../../test_helpers';
+import { rerender, getPageTitle } from '../../../test_helpers';
import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention';
import { ApiLogsTable, NewApiEventsPrompt } from './components';
@@ -42,7 +39,7 @@ describe('ApiLogs', () => {
it('renders', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs');
+ expect(getPageTitle(wrapper)).toEqual('API Logs');
expect(wrapper.find(ApiLogsTable)).toHaveLength(1);
expect(wrapper.find(NewApiEventsPrompt)).toHaveLength(1);
@@ -50,11 +47,20 @@ describe('ApiLogs', () => {
expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api');
});
- it('renders a loading screen', () => {
- setMockValues({ ...values, dataLoading: true, apiLogs: [] });
- const wrapper = shallow( );
+ describe('loading state', () => {
+ it('renders a full-page loading state on initial page load (no logs exist yet)', () => {
+ setMockValues({ ...values, dataLoading: true, apiLogs: [] });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+
+ it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => {
+ setMockValues({ ...values, dataLoading: true, apiLogs: [{}] });
+ const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(false);
+ });
});
describe('effects', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
index b8179163c93f9..d3eef77db21f0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
@@ -9,25 +9,14 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import {
- EuiPageHeader,
- EuiTitle,
- EuiPageContent,
- EuiPageContentBody,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
-} from '@elastic/eui';
-
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
+import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention';
import { ApiLogFlyout } from './api_log';
-import { ApiLogsTable, NewApiEventsPrompt } from './components';
+import { ApiLogsTable, NewApiEventsPrompt, EmptyState } from './components';
import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants';
import { ApiLogsLogic } from './';
@@ -44,38 +33,36 @@ export const ApiLogs: React.FC = () => {
pollForApiLogs();
}, []);
- if (dataLoading && !apiLogs.length) return ;
-
return (
- <>
-
-
-
-
+ }
+ >
-
-
-
-
-
- {RECENT_API_EVENTS}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {RECENT_API_EVENTS}
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- >
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
index 2a00cc6eb42bb..82d3d4715cbc5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui';
+import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty } from '@elastic/eui';
import { DEFAULT_META } from '../../../../shared/constants';
import { mountWithIntl } from '../../../../test_helpers';
@@ -91,14 +91,6 @@ describe('ApiLogsTable', () => {
expect(actions.openFlyout).toHaveBeenCalled();
});
- it('renders an empty prompt if no items are passed', () => {
- setMockValues({ ...values, apiLogs: [] });
- const wrapper = mountWithIntl( );
- const promptContent = wrapper.find(EuiEmptyPrompt).text();
-
- expect(promptContent).toContain('Perform your first API call');
- });
-
describe('hasPagination', () => {
it('does not render with pagination by default', () => {
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
index bb1327ce2da30..d5bb525cfd332 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
@@ -15,7 +15,6 @@ import {
EuiBadge,
EuiHealth,
EuiButtonEmpty,
- EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative } from '@kbn/i18n/react';
@@ -27,6 +26,8 @@ import { ApiLogsLogic } from '../index';
import { ApiLog } from '../types';
import { getStatusColor } from '../utils';
+import { EmptyState } from './';
+
import './api_logs_table.scss';
interface Props {
@@ -109,25 +110,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => {
items={apiLogs}
responsive
loading={dataLoading}
- noItemsMessage={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
- defaultMessage: 'Perform your first API call',
- })}
-
- }
- body={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
- defaultMessage: "Check back after you've performed some API calls.",
- })}
-
- }
- />
- }
+ noItemsMessage={ }
{...paginationProps}
/>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx
new file mode 100644
index 0000000000000..19f45ced5dc5d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+
+import { EmptyState } from './';
+
+describe('EmptyState', () => {
+ it('renders', () => {
+ const wrapper = shallow( )
+ .find(EuiEmptyPrompt)
+ .dive();
+
+ expect(wrapper.find('h2').text()).toEqual('No API events in the last 24 hours');
+ expect(wrapper.find(EuiButton).prop('href')).toEqual(
+ expect.stringContaining('/api-reference.html')
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx
new file mode 100644
index 0000000000000..76bd0cba1731f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { DOCS_PREFIX } from '../../../routes';
+
+export const EmptyState: React.FC = () => (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
+ defaultMessage: 'No API events in the last 24 hours',
+ })}
+
+ }
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
+ defaultMessage: 'Logs will update in real-time when an API request occurs.',
+ })}
+
+ }
+ actions={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.empty.buttonLabel', {
+ defaultMessage: 'View the API reference',
+ })}
+
+ }
+ />
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
index c0edc51d06228..863216554a540 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
@@ -7,3 +7,4 @@
export { ApiLogsTable } from './api_logs_table';
export { NewApiEventsPrompt } from './new_api_events_prompt';
+export { EmptyState } from './empty_state';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx
index a2993b4d86d5a..91a0a7c5edcc0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx
@@ -7,29 +7,25 @@
import React from 'react';
-import {
- EuiButton,
- EuiLink,
- EuiPageHeader,
- EuiPanel,
- EuiSpacer,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiButton, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getAppSearchUrl } from '../../../shared/enterprise_search_url';
import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes';
-import { generateEnginePath } from '../engine';
+import { generateEnginePath, getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import './crawler_landing.scss';
import { CRAWLER_TITLE } from '.';
export const CrawlerLanding: React.FC = () => (
-
-
-
-
+
+
@@ -81,5 +77,5 @@ export const CrawlerLanding: React.FC = () => (
-
+
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
index affc2fd08e34c..3804ecfe7c67d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
@@ -7,14 +7,12 @@
import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';
import '../../../__mocks__/shallow_useeffect.mock';
+import '../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
-import { Loading } from '../../../shared/loading';
-import { rerender } from '../../../test_helpers';
-
import { DomainsTable } from './components/domains_table';
import { CrawlerOverview } from './crawler_overview';
@@ -50,11 +48,4 @@ describe('CrawlerOverview', () => {
// TODO test for empty state after it is built in a future PR
});
-
- it('shows a loading state when data is loading', () => {
- setMockValues({ dataLoading: true });
- rerender(wrapper);
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
index 14906378692ed..9e484df35e7a2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
@@ -9,10 +9,8 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiPageHeader } from '@elastic/eui';
-
-import { FlashMessages } from '../../../shared/flash_messages';
-import { Loading } from '../../../shared/loading';
+import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { DomainsTable } from './components/domains_table';
import { CRAWLER_TITLE } from './constants';
@@ -27,15 +25,13 @@ export const CrawlerOverview: React.FC = () => {
fetchCrawlerData();
}, []);
- if (dataLoading) {
- return ;
- }
-
return (
- <>
-
-
+
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx
index c11c656333010..587ba61ce27e9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx
@@ -5,9 +5,6 @@
* 2.0.
*/
-import { setMockValues } from '../../../__mocks__/kea_logic';
-import { mockEngineValues } from '../../__mocks__';
-
import React from 'react';
import { Switch } from 'react-router-dom';
@@ -22,7 +19,6 @@ describe('CrawlerRouter', () => {
beforeEach(() => {
jest.clearAllMocks();
- setMockValues({ ...mockEngineValues });
});
afterEach(() => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx
index 926c45b437937..a0145cf76908a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx
@@ -8,11 +8,6 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-
-import { getEngineBreadcrumbs } from '../engine';
-
-import { CRAWLER_TITLE } from './constants';
import { CrawlerLanding } from './crawler_landing';
import { CrawlerOverview } from './crawler_overview';
@@ -20,7 +15,6 @@ export const CrawlerRouter: React.FC = () => {
return (
-
{process.env.NODE_ENV === 'development' ? : }
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts
index 37c1e9a7a1a2e..c490910184a69 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts
@@ -18,7 +18,7 @@ export const CURATIONS_OVERVIEW_TITLE = i18n.translate(
);
export const CREATE_NEW_CURATION_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.create.title',
- { defaultMessage: 'Create new curation' }
+ { defaultMessage: 'Create a curation' }
);
export const MANAGE_CURATION_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.manage.title',
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx
index 937acfd84ce83..2efe1f2ffe86f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx
@@ -8,16 +8,13 @@
import '../../../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../__mocks__/react_router';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
-
-import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
-import { Loading } from '../../../../shared/loading';
-import { rerender } from '../../../../test_helpers';
+import { rerender, getPageTitle, getPageHeaderActions } from '../../../../test_helpers';
jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() }));
import { CurationLogic } from './curation_logic';
@@ -27,9 +24,6 @@ import { AddResultFlyout } from './results';
import { Curation } from './';
describe('Curation', () => {
- const props = {
- curationsBreadcrumb: ['Engines', 'some-engine', 'Curations'],
- };
const values = {
dataLoading: false,
queries: ['query A', 'query B'],
@@ -47,39 +41,34 @@ describe('Curation', () => {
});
it('renders', () => {
- const wrapper = shallow( );
+ const wrapper = shallow( );
- expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Manage curation');
- expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([
- ...props.curationsBreadcrumb,
+ expect(getPageTitle(wrapper)).toEqual('Manage curation');
+ expect(wrapper.prop('pageChrome')).toEqual([
+ 'Engines',
+ 'some-engine',
+ 'Curations',
'query A, query B',
]);
});
- it('renders a loading component on page load', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders the add result flyout when open', () => {
setMockValues({ ...values, isFlyoutOpen: true });
- const wrapper = shallow( );
+ const wrapper = shallow( );
expect(wrapper.find(AddResultFlyout)).toHaveLength(1);
});
it('initializes CurationLogic with a curationId prop from URL param', () => {
mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' });
- shallow( );
+ shallow( );
expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' });
});
it('calls loadCuration on page load & whenever the curationId URL param changes', () => {
mockUseParams.mockReturnValueOnce({ curationId: 'cur-123456789' });
- const wrapper = shallow( );
+ const wrapper = shallow( );
expect(actions.loadCuration).toHaveBeenCalledTimes(1);
mockUseParams.mockReturnValueOnce({ curationId: 'cur-987654321' });
@@ -92,9 +81,8 @@ describe('Curation', () => {
let confirmSpy: jest.SpyInstance;
beforeAll(() => {
- const wrapper = shallow( );
- const headerActions = wrapper.find(EuiPageHeader).prop('rightSideItems');
- restoreDefaultsButton = shallow(headerActions![0] as React.ReactElement);
+ const wrapper = shallow( );
+ restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0);
confirmSpy = jest.spyOn(window, 'confirm');
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx
index ffa9fd8422a1b..2a01c0db049ab 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx
@@ -10,26 +10,19 @@ import { useParams } from 'react-router-dom';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
-
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
-import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
-import { Loading } from '../../../../shared/loading';
+import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants';
+import { AppSearchPageTemplate } from '../../layout';
import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants';
+import { getCurationsBreadcrumbs } from '../utils';
import { CurationLogic } from './curation_logic';
import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents';
import { ActiveQuerySelect, ManageQueriesModal } from './queries';
import { AddResultLogic, AddResultFlyout } from './results';
-interface Props {
- curationsBreadcrumb: BreadcrumbTrail;
-}
-
-export const Curation: React.FC = ({ curationsBreadcrumb }) => {
+export const Curation: React.FC = () => {
const { curationId } = useParams() as { curationId: string };
const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId }));
const { dataLoading, queries } = useValues(CurationLogic({ curationId }));
@@ -39,14 +32,12 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => {
loadCuration();
}, [curationId]);
- if (dataLoading) return ;
-
return (
- <>
-
- {
@@ -55,10 +46,10 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => {
>
{RESTORE_DEFAULTS_BUTTON_LABEL}
,
- ]}
- responsive={false}
- />
-
+ ],
+ }}
+ isLoading={dataLoading}
+ >
@@ -69,7 +60,6 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => {
-
@@ -78,6 +68,6 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => {
{isFlyoutOpen && }
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx
index f2bc416b00341..8cb06f32d9e4e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx
@@ -80,7 +80,7 @@ export const HiddenDocuments: React.FC = () => {
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyTitle',
- { defaultMessage: 'No documents are being hidden for this query' }
+ { defaultMessage: "You haven't hidden any documents yet" }
)}
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
index 9598212d3e0c9..a241edb8020a4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
@@ -19,6 +19,6 @@ describe('CurationsRouter', () => {
const wrapper = shallow( );
expect(wrapper.find(Switch)).toHaveLength(1);
- expect(wrapper.find(Route)).toHaveLength(4);
+ expect(wrapper.find(Route)).toHaveLength(3);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
index 28ce311b43887..40f2d07ab61ab 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
@@ -8,38 +8,26 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
-import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { NotFound } from '../../../shared/not_found';
import {
ENGINE_CURATIONS_PATH,
ENGINE_CURATIONS_NEW_PATH,
ENGINE_CURATION_PATH,
} from '../../routes';
-import { getEngineBreadcrumbs } from '../engine';
-import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants';
import { Curation } from './curation';
import { Curations, CurationCreation } from './views';
export const CurationsRouter: React.FC = () => {
- const CURATIONS_BREADCRUMB = getEngineBreadcrumbs([CURATIONS_TITLE]);
-
return (
-
-
-
-
-
-
+
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts
index 51618ed4e3741..02641b09255e5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts
@@ -5,7 +5,21 @@
* 2.0.
*/
-import { convertToDate, addDocument, removeDocument } from './utils';
+import '../../__mocks__/engine_logic.mock';
+
+import { getCurationsBreadcrumbs, convertToDate, addDocument, removeDocument } from './utils';
+
+describe('getCurationsBreadcrumbs', () => {
+ it('generates curation-prefixed breadcrumbs', () => {
+ expect(getCurationsBreadcrumbs()).toEqual(['Engines', 'some-engine', 'Curations']);
+ expect(getCurationsBreadcrumbs(['Some page'])).toEqual([
+ 'Engines',
+ 'some-engine',
+ 'Curations',
+ 'Some page',
+ ]);
+ });
+});
describe('convertToDate', () => {
it('converts the English-only server timestamps to a parseable Date', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts
index 8af2636128304..978b63885fbdd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts
@@ -5,6 +5,14 @@
* 2.0.
*/
+import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
+import { getEngineBreadcrumbs } from '../engine';
+
+import { CURATIONS_TITLE } from './constants';
+
+export const getCurationsBreadcrumbs = (breadcrumbs: BreadcrumbTrail = []) =>
+ getEngineBreadcrumbs([CURATIONS_TITLE, ...breadcrumbs]);
+
// The server API feels us an English datestring, but we want to convert
// it to an actual Date() instance so that we can localize date formats.
export const convertToDate = (serverDateString: string): Date => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx
index ad306dfc73080..33aab9943cc83 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx
@@ -6,6 +6,7 @@
*/
import { setMockActions } from '../../../../__mocks__/kea_logic';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx
index 32d46775a2125..9aa1759cec5c0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx
@@ -9,10 +9,10 @@ import React from 'react';
import { useActions } from 'kea';
-import { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
+import { EuiPanel, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
+import { AppSearchPageTemplate } from '../../layout';
import { MultiInputRows } from '../../multi_input_rows';
import {
@@ -21,15 +21,17 @@ import {
QUERY_INPUTS_PLACEHOLDER,
} from '../constants';
import { CurationsLogic } from '../index';
+import { getCurationsBreadcrumbs } from '../utils';
export const CurationCreation: React.FC = () => {
const { createCuration } = useActions(CurationsLogic);
return (
- <>
-
-
-
+
+
{i18n.translate(
@@ -56,7 +58,7 @@ export const CurationCreation: React.FC = () => {
inputPlaceholder={QUERY_INPUTS_PLACEHOLDER}
onSubmit={(queries) => createCuration(queries)}
/>
-
- >
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx
index bcc402d6eea27..85827d5374179 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx
@@ -6,17 +6,16 @@
*/
import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
+import '../../../../__mocks__/react_router';
import '../../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow, ReactWrapper } from 'enzyme';
-import { EuiPageHeader, EuiBasicTable } from '@elastic/eui';
+import { EuiBasicTable } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
-import { mountWithIntl } from '../../../../test_helpers';
-import { EmptyState } from '../components';
+import { mountWithIntl, getPageTitle } from '../../../../test_helpers';
import { Curations, CurationsTable } from './curations';
@@ -61,32 +60,34 @@ describe('Curations', () => {
it('renders', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Curated results');
+ expect(getPageTitle(wrapper)).toEqual('Curated results');
expect(wrapper.find(CurationsTable)).toHaveLength(1);
});
- it('renders a loading component on page load', () => {
- setMockValues({ ...values, dataLoading: true, curations: [] });
- const wrapper = shallow( );
+ describe('loading state', () => {
+ it('renders a full-page loading state on initial page load', () => {
+ setMockValues({ ...values, dataLoading: true, curations: [] });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+
+ it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => {
+ setMockValues({ ...values, dataLoading: true, curations: [{}] });
+ const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(false);
+ });
});
it('calls loadCurations on page load', () => {
+ setMockValues({ ...values, myRole: {} }); // Required for AppSearchPageTemplate to load
mountWithIntl( );
expect(actions.loadCurations).toHaveBeenCalledTimes(1);
});
describe('CurationsTable', () => {
- it('renders an empty state', () => {
- setMockValues({ ...values, curations: [] });
- const table = shallow( ).find(EuiBasicTable);
- const noItemsMessage = table.prop('noItemsMessage') as React.ReactElement;
-
- expect(noItemsMessage.type).toEqual(EmptyState);
- });
-
it('passes loading prop based on dataLoading', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx
index 80de9aba77258..12497ab52baf6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx
@@ -9,25 +9,24 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiBasicTable, EuiBasicTableColumn, EuiPageContent, EuiPageHeader } from '@elastic/eui';
+import { EuiBasicTable, EuiBasicTableColumn, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants';
-import { FlashMessages } from '../../../../shared/flash_messages';
import { KibanaLogic } from '../../../../shared/kibana';
-import { Loading } from '../../../../shared/loading';
import { EuiButtonTo, EuiLinkTo } from '../../../../shared/react_router_helpers';
import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination';
import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes';
import { FormattedDateTime } from '../../../utils/formatted_date_time';
import { generateEnginePath } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
import { EmptyState } from '../components';
import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants';
import { CurationsLogic } from '../curations_logic';
import { Curation } from '../types';
-import { convertToDate } from '../utils';
+import { getCurationsBreadcrumbs, convertToDate } from '../utils';
export const Curations: React.FC = () => {
const { dataLoading, curations, meta } = useValues(CurationsLogic);
@@ -37,23 +36,29 @@ export const Curations: React.FC = () => {
loadCurations();
}, [meta.page.current]);
- if (dataLoading && !curations.length) return ;
-
return (
- <>
-
+
{CREATE_NEW_CURATION_TITLE}
,
- ]}
- />
-
-
+ ],
+ }}
+ isLoading={dataLoading && !curations.length}
+ isEmptyState={!curations.length}
+ emptyState={ }
+ >
+
-
- >
+
+
);
};
@@ -139,7 +144,6 @@ export const CurationsTable: React.FC = () => {
responsive
hasActions
loading={dataLoading}
- noItemsMessage={ }
pagination={{
...convertMetaToPagination(meta),
hidePerPageOptions: true,
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index 3e18c9e680de2..0f42483f44e0c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -13,9 +13,7 @@ import { useValues, useActions } from 'kea';
import { i18n } from '@kbn/i18n';
import { setQueuedErrorMessage } from '../../../shared/flash_messages';
-import { Layout } from '../../../shared/layout';
import { AppLogic } from '../../app_logic';
-import { AppSearchNav } from '../../index';
import {
ENGINE_PATH,
@@ -109,54 +107,51 @@ export const EngineRouter: React.FC = () => {
)}
+ {canViewEngineSchema && (
+
+
+
+ )}
+ {canViewMetaEngineSourceEngines && (
+
+
+
+ )}
+ {canViewEngineCrawler && (
+
+
+
+ )}
+ {canManageEngineRelevanceTuning && (
+
+
+
+ )}
+ {canManageEngineSynonyms && (
+
+
+
+ )}
+ {canManageEngineCurations && (
+
+
+
+ )}
+ {canManageEngineResultSettings && (
+
+
+
+ )}
{canManageEngineSearchUi && (
)}
- {/* TODO: Remove layout once page template migration is over */}
- }>
- {canViewEngineSchema && (
-
-
-
- )}
- {canManageEngineCurations && (
-
-
-
- )}
- {canManageEngineRelevanceTuning && (
-
-
-
- )}
- {canManageEngineSynonyms && (
-
-
-
- )}
- {canManageEngineResultSettings && (
-
-
-
- )}
- {canViewEngineApiLogs && (
-
-
-
- )}
- {canViewMetaEngineSourceEngines && (
-
-
-
- )}
- {canViewEngineCrawler && (
-
-
-
- )}
-
+ {canViewEngineApiLogs && (
+
+
+
+ )}
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx
index 913aa4f0ec845..18b8390081467 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx
@@ -22,6 +22,7 @@ import {
EuiButton,
} from '@elastic/eui';
+import { ENGINES_TITLE } from '../engines';
import { AppSearchPageTemplate } from '../layout';
import {
@@ -43,7 +44,7 @@ export const EngineCreation: React.FC = () => {
return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx
index 159a986096ae2..9117fdd0be87d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx
@@ -53,7 +53,7 @@ describe('EmptyState', () => {
});
it('sends a user to engine creation', () => {
- expect(button.prop('to')).toEqual('/engine_creation');
+ expect(button.prop('to')).toEqual('/engines/new');
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts
index 1d8e578e0edf2..63235f8a992f0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts
@@ -5,6 +5,5 @@
* 2.0.
*/
-export { LaunchAppSearchButton } from './launch_as_button';
export { EmptyState } from './empty_state';
export { EmptyMetaEnginesState } from './empty_meta_engines_state';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx
deleted file mode 100644
index 93c91cc3830f4..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import '../../../../__mocks__/enterprise_search_url.mock';
-import { mockTelemetryActions } from '../../../../__mocks__/kea_logic';
-
-import React from 'react';
-
-import { shallow } from 'enzyme';
-
-import { LaunchAppSearchButton } from './';
-
-describe('LaunchAppSearchButton', () => {
- it('renders a launch app search button that sends telemetry on click', () => {
- const button = shallow( );
-
- expect(button.prop('href')).toBe('http://localhost:3002/as');
- expect(button.prop('isDisabled')).toBeFalsy();
-
- button.simulate('click');
- expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalled();
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx
deleted file mode 100644
index 41102cb4fba2e..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { useActions } from 'kea';
-
-import { EuiButton } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-import { getAppSearchUrl } from '../../../../shared/enterprise_search_url';
-import { TelemetryLogic } from '../../../../shared/telemetry';
-
-export const LaunchAppSearchButton: React.FC = () => {
- const { sendAppSearchTelemetry } = useActions(TelemetryLogic);
-
- return (
- // eslint-disable-next-line @elastic/eui/href-or-on-click
-
- sendAppSearchTelemetry({
- action: 'clicked',
- metric: 'header_launch_button',
- })
- }
- data-test-subj="launchButton"
- >
- {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', {
- defaultMessage: 'Launch App Search',
- })}
-
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
index 4dff246052138..d1dd5514757d7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
@@ -20,7 +20,7 @@ import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes';
import { DataPanel } from '../data_panel';
import { AppSearchPageTemplate } from '../layout';
-import { LaunchAppSearchButton, EmptyState, EmptyMetaEnginesState } from './components';
+import { EmptyState, EmptyMetaEnginesState } from './components';
import { EnginesTable } from './components/tables/engines_table';
import { MetaEnginesTable } from './components/tables/meta_engines_table';
import {
@@ -65,10 +65,7 @@ export const EnginesOverview: React.FC = () => {
],
- }}
+ pageHeader={{ pageTitle: ENGINES_OVERVIEW_TITLE }}
isLoading={dataLoading}
isEmptyState={!engines.length}
emptyState={ }
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
index 80230394ce2a2..c9f5452e254e1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
@@ -8,7 +8,7 @@
import { setMockValues } from '../../../__mocks__/kea_logic';
jest.mock('../../../shared/layout', () => ({
- generateNavLink: jest.fn(({ to }) => ({ href: to })),
+ generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })),
}));
jest.mock('../engine/engine_nav', () => ({
useEngineNav: () => [],
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
index 4737fbcf07e23..c3b8ec642233b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
@@ -28,8 +28,12 @@ export const useAppSearchNav = () => {
{
id: 'engines',
name: ENGINES_TITLE,
- ...generateNavLink({ to: ENGINES_PATH, isRoot: true }),
- items: useEngineNav(),
+ ...generateNavLink({
+ to: ENGINES_PATH,
+ isRoot: true,
+ shouldShowActiveForSubroutes: true,
+ items: useEngineNav(),
+ }),
},
];
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx
index 325e557acec0c..1455444ab2b4b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx
@@ -25,6 +25,7 @@ import {
} from '@elastic/eui';
import { AppLogic } from '../../app_logic';
+import { ENGINES_TITLE } from '../engines';
import { AppSearchPageTemplate } from '../layout';
import {
@@ -73,7 +74,7 @@ export const MetaEngineCreation: React.FC = () => {
return (
(
-
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', {
- defaultMessage: 'Add documents to tune relevance',
- })}
-
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', {
+ defaultMessage: 'Add documents to tune relevance',
+ })}
+
+ }
+ body={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description',
+ {
+ defaultMessage:
+ 'A schema will be automatically created for you after you index some documents.',
}
- body={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description',
- {
- defaultMessage:
- 'A schema will be automatically created for you after you index some documents.',
- }
- )}
- actions={
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel',
- { defaultMessage: 'Read the relevance tuning guide' }
- )}
-
- }
- />
-
+ )}
+ actions={
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel',
+ { defaultMessage: 'Read the relevance tuning guide' }
+ )}
+
+ }
+ />
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
index 092740ac5d3cc..48b536a954ed5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
@@ -13,14 +13,14 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiEmptyPrompt } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt';
+import { getPageHeaderActions } from '../../../test_helpers';
-import { EmptyState } from './components';
import { RelevanceTuning } from './relevance_tuning';
+
+import { RelevanceTuningCallouts } from './relevance_tuning_callouts';
import { RelevanceTuningForm } from './relevance_tuning_form';
+import { RelevanceTuningPreview } from './relevance_tuning_preview';
describe('RelevanceTuning', () => {
const values = {
@@ -50,9 +50,9 @@ describe('RelevanceTuning', () => {
it('renders', () => {
const wrapper = subject();
+ expect(wrapper.find(RelevanceTuningCallouts).exists()).toBe(true);
expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true);
- expect(wrapper.find(Loading).exists()).toBe(false);
- expect(wrapper.find(EmptyState).exists()).toBe(false);
+ expect(wrapper.find(RelevanceTuningPreview).exists()).toBe(true);
});
it('initializes relevance tuning data', () => {
@@ -60,33 +60,38 @@ describe('RelevanceTuning', () => {
expect(actions.initializeRelevanceTuning).toHaveBeenCalled();
});
- it('will render an empty message when the engine has no schema', () => {
+ it('will prevent user from leaving the page if there are unsaved changes', () => {
setMockValues({
...values,
- engineHasSchemaFields: false,
+ unsavedChanges: true,
});
- const wrapper = subject();
- expect(wrapper.find(EmptyState).dive().find(EuiEmptyPrompt).exists()).toBe(true);
- expect(wrapper.find(Loading).exists()).toBe(false);
- expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false);
+ expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true);
});
- it('will show a loading message if data is loading', () => {
- setMockValues({
- ...values,
- dataLoading: true,
+ describe('header actions', () => {
+ it('renders a Save button that will save the current changes', () => {
+ const buttons = getPageHeaderActions(subject());
+ expect(buttons.children().length).toBe(2);
+ const saveButton = buttons.find('[data-test-subj="SaveRelevanceTuning"]');
+ saveButton.simulate('click');
+ expect(actions.updateSearchSettings).toHaveBeenCalled();
});
- const wrapper = subject();
- expect(wrapper.find(Loading).exists()).toBe(true);
- expect(wrapper.find(EmptyState).exists()).toBe(false);
- expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false);
- });
- it('will prevent user from leaving the page if there are unsaved changes', () => {
- setMockValues({
- ...values,
- unsavedChanges: true,
+ it('renders a Reset button that will remove all weights and boosts', () => {
+ const buttons = getPageHeaderActions(subject());
+ expect(buttons.children().length).toBe(2);
+ const resetButton = buttons.find('[data-test-subj="ResetRelevanceTuning"]');
+ resetButton.simulate('click');
+ expect(actions.resetSearchSettings).toHaveBeenCalled();
+ });
+
+ it('will not render buttons if the engine has no schema', () => {
+ setMockValues({
+ ...values,
+ engineHasSchemaFields: false,
+ });
+ const buttons = getPageHeaderActions(subject());
+ expect(buttons.children().length).toBe(0);
});
- expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
index b98541a963890..2e87d6836199b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
@@ -9,43 +9,77 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { Loading } from '../../../shared/loading';
+import { SAVE_BUTTON_LABEL } from '../../../shared/constants';
import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt';
+import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants';
+import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { EmptyState } from './components';
+import { RELEVANCE_TUNING_TITLE } from './constants';
+import { RelevanceTuningCallouts } from './relevance_tuning_callouts';
import { RelevanceTuningForm } from './relevance_tuning_form';
-import { RelevanceTuningLayout } from './relevance_tuning_layout';
import { RelevanceTuningPreview } from './relevance_tuning_preview';
import { RelevanceTuningLogic } from '.';
export const RelevanceTuning: React.FC = () => {
const { dataLoading, engineHasSchemaFields, unsavedChanges } = useValues(RelevanceTuningLogic);
- const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic);
+ const { initializeRelevanceTuning, resetSearchSettings, updateSearchSettings } = useActions(
+ RelevanceTuningLogic
+ );
useEffect(() => {
initializeRelevanceTuning();
}, []);
- if (dataLoading) return ;
-
return (
-
+
+ {SAVE_BUTTON_LABEL}
+ ,
+
+ {RESTORE_DEFAULTS_BUTTON_LABEL}
+ ,
+ ]
+ : [],
+ }}
+ isLoading={dataLoading}
+ isEmptyState={!engineHasSchemaFields}
+ emptyState={ }
+ >
- {engineHasSchemaFields ? (
-
-
-
-
-
-
-
-
- ) : (
-
- )}
-
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx
index 5cbd291f85deb..c35cd280c7a05 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx
@@ -42,7 +42,7 @@ export const RelevanceTuningForm: React.FC = () => {
return (
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts
index df1e19e264c75..cce18cbeffd0a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts
@@ -9,14 +9,6 @@ import { i18n } from '@kbn/i18n';
import { AdvanceRoleType } from '../../types';
-export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate(
- 'xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage',
- {
- defaultMessage:
- 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.',
- }
-);
-
export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx
index db0e6e6dead11..03e2ae67eca9e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx
@@ -10,16 +10,25 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping';
+import {
+ RoleMappingsTable,
+ RoleMappingsHeading,
+ RolesEmptyPrompt,
+} from '../../../shared/role_mapping';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
+
+import { DOCS_PREFIX } from '../../routes';
import { AppSearchPageTemplate } from '../layout';
import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING } from './constants';
import { RoleMapping } from './role_mapping';
import { RoleMappingsLogic } from './role_mappings_logic';
+const ROLES_DOCS_LINK = `${DOCS_PREFIX}/security-and-users.html`;
+
export const RoleMappings: React.FC = () => {
const {
+ enableRoleBasedAccess,
initializeRoleMappings,
initializeRoleMapping,
handleDeleteMapping,
@@ -37,10 +46,19 @@ export const RoleMappings: React.FC = () => {
return resetState;
}, []);
+ const rolesEmptyState = (
+
+ );
+
const roleMappingsSection = (
initializeRoleMapping()}
/>
{
pageChrome={[ROLE_MAPPINGS_TITLE]}
pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }}
isLoading={dataLoading}
+ isEmptyState={roleMappings.length < 1}
+ emptyState={rolesEmptyState}
>
{roleMappingFlyoutOpen && }
{roleMappingsSection}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
index 870e303a2930d..6985f213d1dd5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
@@ -87,6 +87,13 @@ describe('RoleMappingsLogic', () => {
});
});
+ it('setRoleMappings', () => {
+ RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [asRoleMapping] });
+
+ expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]);
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
+ });
+
it('handleRoleChange', () => {
RoleMappingsLogic.actions.handleRoleChange('dev');
@@ -266,6 +273,30 @@ describe('RoleMappingsLogic', () => {
});
describe('listeners', () => {
+ describe('enableRoleBasedAccess', () => {
+ it('calls API and sets values', async () => {
+ const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings');
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(true);
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/app_search/role_mappings/enable_role_based_access'
+ );
+ await nextTick();
+ expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps);
+ });
+
+ it('handles error', async () => {
+ http.post.mockReturnValue(Promise.reject('this is an error'));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ });
+ });
+
describe('initializeRoleMappings', () => {
it('calls API and sets values', async () => {
const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData');
@@ -400,18 +431,8 @@ describe('RoleMappingsLogic', () => {
});
describe('handleDeleteMapping', () => {
- let confirmSpy: any;
const roleMappingId = 'r1';
- beforeEach(() => {
- confirmSpy = jest.spyOn(window, 'confirm');
- confirmSpy.mockImplementation(jest.fn(() => true));
- });
-
- afterEach(() => {
- confirmSpy.mockRestore();
- });
-
it('calls API and refreshes list', async () => {
mount(mappingsServerProps);
const initializeRoleMappingsSpy = jest.spyOn(
@@ -436,14 +457,6 @@ describe('RoleMappingsLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
});
-
- it('will do nothing if not confirmed', () => {
- mount(mappingsServerProps);
- jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
- RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
-
- expect(http.delete).not.toHaveBeenCalled();
- });
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
index fc0a235b23c77..e2ef75897528c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
@@ -22,7 +22,6 @@ import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
import { Engine } from '../engine/types';
import {
- DELETE_ROLE_MAPPING_MESSAGE,
ROLE_MAPPING_DELETED_MESSAGE,
ROLE_MAPPING_CREATED_MESSAGE,
ROLE_MAPPING_UPDATED_MESSAGE,
@@ -59,10 +58,16 @@ interface RoleMappingsActions {
initializeRoleMappings(): void;
resetState(): void;
setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping };
+ setRoleMappings({
+ roleMappings,
+ }: {
+ roleMappings: ASRoleMapping[];
+ }): { roleMappings: ASRoleMapping[] };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
setRoleMappingErrors(errors: string[]): { errors: string[] };
+ enableRoleBasedAccess(): void;
}
interface RoleMappingsValues {
@@ -91,6 +96,7 @@ export const RoleMappingsLogic = kea data,
setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }),
+ setRoleMappings: ({ roleMappings }: { roleMappings: ASRoleMapping[] }) => ({ roleMappings }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string) => ({ value }),
handleRoleChange: (roleType: RoleTypes) => ({ roleType }),
@@ -101,6 +107,7 @@ export const RoleMappingsLogic = kea ({ value }),
handleAccessAllEnginesChange: (selected: boolean) => ({ selected }),
+ enableRoleBasedAccess: true,
resetState: true,
initializeRoleMappings: true,
initializeRoleMapping: (roleMappingId) => ({ roleMappingId }),
@@ -114,13 +121,16 @@ export const RoleMappingsLogic = kea false,
+ setRoleMappings: () => false,
resetState: () => true,
+ enableRoleBasedAccess: () => true,
},
],
roleMappings: [
[],
{
setRoleMappingsData: (_, { roleMappings }) => roleMappings,
+ setRoleMappings: (_, { roleMappings }) => roleMappings,
resetState: () => [],
},
],
@@ -267,6 +277,17 @@ export const RoleMappingsLogic = kea ({
+ enableRoleBasedAccess: async () => {
+ const { http } = HttpLogic.values;
+ const route = '/api/app_search/role_mappings/enable_role_based_access';
+
+ try {
+ const response = await http.post(route);
+ actions.setRoleMappings(response);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
initializeRoleMappings: async () => {
const { http } = HttpLogic.values;
const route = '/api/app_search/role_mappings';
@@ -286,14 +307,12 @@ export const RoleMappingsLogic = kea {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx
index ea658c741b8a0..1b353f17855d2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx
@@ -5,12 +5,16 @@
* 2.0.
*/
+import { setMockValues } from '../../../../__mocks__/kea_logic';
+
import React from 'react';
import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { SchemaAddFieldModal } from '../../../../shared/schema';
+
import { EmptyState } from './';
describe('EmptyState', () => {
@@ -24,4 +28,11 @@ describe('EmptyState', () => {
expect.stringContaining('#indexing-documents-guide-schema')
);
});
+
+ it('renders a modal that lets a user add a new schema field', () => {
+ setMockValues({ isModalOpen: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(SchemaAddFieldModal)).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx
index 6d7dd198d5eef..ad9285c7b8fef 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx
@@ -7,14 +7,21 @@
import React from 'react';
-import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { useValues, useActions } from 'kea';
+
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { SchemaAddFieldModal } from '../../../../shared/schema';
import { DOCS_PREFIX } from '../../../routes';
+import { SchemaLogic } from '../schema_logic';
export const EmptyState: React.FC = () => {
+ const { isModalOpen } = useValues(SchemaLogic);
+ const { addSchemaField, closeModal } = useActions(SchemaLogic);
+
return (
-
+ <>
{
}
/>
-
+ {isModalOpen && (
+
+ )}
+ >
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx
index e76ab60005231..4dd7a869ca27e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx
@@ -14,15 +14,11 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { Loading } from '../../../../shared/loading';
import { SchemaErrorsAccordion } from '../../../../shared/schema';
import { ReindexJob } from './';
describe('ReindexJob', () => {
- const props = {
- schemaBreadcrumb: ['Engines', 'some-engine', 'Schema'],
- };
const values = {
dataLoading: false,
fieldCoercionErrors: {},
@@ -43,27 +39,20 @@ describe('ReindexJob', () => {
});
it('renders', () => {
- const wrapper = shallow( );
+ const wrapper = shallow( );
expect(wrapper.find(SchemaErrorsAccordion)).toHaveLength(1);
expect(wrapper.find(SchemaErrorsAccordion).prop('generateViewPath')).toHaveLength(1);
});
it('calls loadReindexJob on page load', () => {
- shallow( );
+ shallow( );
expect(actions.loadReindexJob).toHaveBeenCalledWith('abc1234567890');
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders schema errors with links to document pages', () => {
- const wrapper = shallow( );
+ const wrapper = shallow( );
const generateViewPath = wrapper
.find(SchemaErrorsAccordion)
.prop('generateViewPath') as Function;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx
index 576b4ae11603b..b0a8cbd25f8b0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx
@@ -10,25 +10,17 @@ import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
-import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
-import { Loading } from '../../../../shared/loading';
import { SchemaErrorsAccordion } from '../../../../shared/schema';
-
import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../routes';
-import { EngineLogic, generateEnginePath } from '../../engine';
+import { EngineLogic, generateEnginePath, getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
+import { SCHEMA_TITLE } from '../constants';
import { ReindexJobLogic } from './reindex_job_logic';
-interface Props {
- schemaBreadcrumb: BreadcrumbTrail;
-}
-
-export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => {
+export const ReindexJob: React.FC = () => {
const { reindexJobId } = useParams() as { reindexJobId: string };
const { loadReindexJob } = useActions(ReindexJobLogic);
const { dataLoading, fieldCoercionErrors } = useValues(ReindexJobLogic);
@@ -40,34 +32,29 @@ export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => {
loadReindexJob(reindexJobId);
}, [reindexJobId]);
- if (dataLoading) return ;
-
return (
- <>
-
-
+
+ generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { documentId })
+ }
/>
-
-
-
- generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { documentId })
- }
- />
-
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts
index 7687296cf9f83..dcc5747b0d32f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts
@@ -140,13 +140,13 @@ describe('SchemaLogic', () => {
describe('selectors', () => {
describe('hasSchema', () => {
- it('returns true when the schema obj has items', () => {
- mountAndSetSchema({ schema: { test: SchemaType.Text } });
+ it('returns true when the cached server schema obj has items', () => {
+ mount({ cachedSchema: { test: SchemaType.Text } });
expect(SchemaLogic.values.hasSchema).toEqual(true);
});
- it('returns false when the schema obj is empty', () => {
- mountAndSetSchema({ schema: {} });
+ it('returns false when the cached server schema obj is empty', () => {
+ mount({ schema: {} });
expect(SchemaLogic.values.hasSchema).toEqual(false);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts
index 3215a46c8e299..3dcafd6782afd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts
@@ -108,7 +108,10 @@ export const SchemaLogic = kea>({
],
},
selectors: {
- hasSchema: [(selectors) => [selectors.schema], (schema) => Object.keys(schema).length > 0],
+ hasSchema: [
+ (selectors) => [selectors.cachedSchema],
+ (cachedSchema) => Object.keys(cachedSchema).length > 0,
+ ],
hasSchemaChanged: [
(selectors) => [selectors.schema, selectors.cachedSchema],
(schema, cachedSchema) => !isEqual(schema, cachedSchema),
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx
index bfa346fee468b..d358c489593c5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx
@@ -10,27 +10,21 @@ import { Route, Switch } from 'react-router-dom';
import { useValues } from 'kea';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { ENGINE_REINDEX_JOB_PATH } from '../../routes';
-import { EngineLogic, getEngineBreadcrumbs } from '../engine';
+import { EngineLogic } from '../engine';
-import { SCHEMA_TITLE } from './constants';
import { ReindexJob } from './reindex_job';
import { Schema, MetaEngineSchema } from './views';
export const SchemaRouter: React.FC = () => {
const { isMetaEngine } = useValues(EngineLogic);
- const schemaBreadcrumb = getEngineBreadcrumbs([SCHEMA_TITLE]);
return (
-
-
-
-
- {isMetaEngine ? : }
+
+ {isMetaEngine ? : }
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
index 1d677ad08db43..60a0513b774fd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
@@ -7,6 +7,7 @@
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
import '../../../../__mocks__/shallow_useeffect.mock';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
@@ -14,8 +15,6 @@ import { shallow } from 'enzyme';
import { EuiCallOut } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
-
import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components';
import { MetaEngineSchema } from './';
@@ -46,13 +45,6 @@ describe('MetaEngineSchema', () => {
expect(actions.loadSchema).toHaveBeenCalled();
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders an inactive fields callout & table when source engines have schema conflicts', () => {
setMockValues({ ...values, hasConflicts: true, conflictingFieldsCount: 5 });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
index 4c0235cf81129..2eb8bac00a040 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
@@ -9,14 +9,15 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiPageContentBody, EuiCallOut, EuiSpacer } from '@elastic/eui';
+import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { Loading } from '../../../../shared/loading';
import { DataPanel } from '../../data_panel';
+import { getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components';
+import { SCHEMA_TITLE } from '../constants';
import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic';
export const MetaEngineSchema: React.FC = () => {
@@ -27,90 +28,88 @@ export const MetaEngineSchema: React.FC = () => {
loadSchema();
}, []);
- if (dataLoading) return ;
-
return (
- <>
-
-
-
- {hasConflicts && (
- <>
-
+ {hasConflicts && (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription',
{
defaultMessage:
- '{conflictingFieldsCount, plural, one {# field is} other {# fields are}} not searchable',
- values: { conflictingFieldsCount },
+ 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.',
}
)}
- >
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription',
- {
- defaultMessage:
- 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.',
- }
- )}
-
-
-
- >
+
+
+
+ >
+ )}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
+ { defaultMessage: 'Active fields' }
+ )}
+
+ }
+ subtitle={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
+ { defaultMessage: 'Fields which belong to one or more engine.' }
)}
+ >
+
+
+
+ {hasConflicts && (
{i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
- { defaultMessage: 'Active fields' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
+ { defaultMessage: 'Inactive fields' }
)}
}
subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
- { defaultMessage: 'Fields which belong to one or more engine.' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
+ {
+ defaultMessage:
+ 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
+ }
)}
>
-
+
-
- {hasConflicts && (
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
- { defaultMessage: 'Inactive fields' }
- )}
-
- }
- subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
- {
- defaultMessage:
- 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
- }
- )}
- >
-
-
- )}
-
- >
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
index 91ec8eda55fc3..cae16d70592fa 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
@@ -7,17 +7,18 @@
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
import '../../../../__mocks__/shallow_useeffect.mock';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader, EuiButton } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getPageHeaderActions } from '../../../../test_helpers';
-import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SchemaCallouts, SchemaTable } from '../components';
import { Schema } from './';
@@ -56,27 +57,8 @@ describe('Schema', () => {
expect(actions.loadSchema).toHaveBeenCalled();
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
- it('renders an empty state', () => {
- setMockValues({ ...values, hasSchema: false });
- const wrapper = shallow( );
-
- expect(wrapper.find(EmptyState)).toHaveLength(1);
- });
-
describe('page action buttons', () => {
- const subject = () =>
- shallow( )
- .find(EuiPageHeader)
- .dive()
- .children()
- .dive();
+ const subject = () => getPageHeaderActions(shallow( ));
it('renders', () => {
const wrapper = subject();
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
index 7bc995b16468a..d2a760e8accff 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
@@ -9,14 +9,15 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiButton, EuiPageContentBody } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SCHEMA_TITLE } from '../constants';
import { SchemaLogic } from '../schema_logic';
export const Schema: React.FC = () => {
@@ -31,19 +32,18 @@ export const Schema: React.FC = () => {
loadSchema();
}, []);
- if (dataLoading) return ;
-
return (
- <>
- {
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.schema.updateSchemaButtonLabel',
- { defaultMessage: 'Update types' }
+ { defaultMessage: 'Save changes' }
)}
,
{
{ defaultMessage: 'Create a schema field' }
)}
,
- ]}
- />
-
-
-
- {hasSchema ? : }
- {isModalOpen && (
-
- )}
-
- >
+ ],
+ }}
+ isLoading={dataLoading}
+ isEmptyState={!hasSchema}
+ emptyState={ }
+ >
+
+
+ {isModalOpen && (
+
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
index 004217d88987b..3076e14d6329b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
@@ -18,7 +18,7 @@ export const AddSourceEnginesButton: React.FC = () => {
const { openModal } = useActions(SourceEnginesLogic);
return (
-
+
{ADD_SOURCE_ENGINES_BUTTON_LABEL}
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
index 9d2fe653150c3..e2398209e630d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
@@ -11,11 +11,9 @@ import '../../__mocks__/engine_logic.mock';
import React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
+import { getPageHeaderActions } from '../../../test_helpers';
import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components';
@@ -61,20 +59,10 @@ describe('SourceEngines', () => {
expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1);
});
- it('renders a loading component before data has loaded', () => {
- setMockValues({ ...MOCK_VALUES, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
describe('page actions', () => {
- const getPageHeader = (wrapper: ShallowWrapper) =>
- wrapper.find(EuiPageHeader).dive().children().dive();
-
it('contains a button to add source engines', () => {
const wrapper = shallow( );
- expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1);
+ expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(1);
});
it('hides the add source engines button if the user does not have permissions', () => {
@@ -86,7 +74,7 @@ describe('SourceEngines', () => {
});
const wrapper = shallow( );
- expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0);
+ expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(0);
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
index 190c44c919020..d2476faf4f3f5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
@@ -9,13 +9,11 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiPageHeader, EuiPageContent } from '@elastic/eui';
+import { EuiPanel } from '@elastic/eui';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
import { AppLogic } from '../../app_logic';
import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components';
import { SOURCE_ENGINES_TITLE } from './i18n';
@@ -33,20 +31,19 @@ export const SourceEngines: React.FC = () => {
fetchSourceEngines();
}, []);
- if (dataLoading) return ;
-
return (
- <>
-
- ] : []}
- />
-
-
+ ] : [],
+ }}
+ isLoading={dataLoading}
+ >
+
{isModalOpen && }
-
- >
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx
index f1382bb5972b2..a43f170e5822f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx
@@ -11,7 +11,7 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
-import { EmptyState } from './';
+import { EmptyState, SynonymModal } from './';
describe('EmptyState', () => {
it('renders', () => {
@@ -24,4 +24,10 @@ describe('EmptyState', () => {
expect.stringContaining('/synonyms-guide.html')
);
});
+
+ it('renders the add synonym modal', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(SynonymModal)).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx
index 2eb6643bda503..f856a5c035f81 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx
@@ -7,16 +7,16 @@
import React from 'react';
-import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DOCS_PREFIX } from '../../../routes';
-import { SynonymIcon } from './';
+import { SynonymModal, SynonymIcon } from './';
export const EmptyState: React.FC = () => {
return (
-
+ <>
{
}
/>
-
+
+ >
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx
index c8f65c4bdbc6c..64ac3066b51a5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx
@@ -13,12 +13,11 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader, EuiButton, EuiPagination } from '@elastic/eui';
+import { EuiButton, EuiPagination } from '@elastic/eui';
-import { Loading } from '../../../shared/loading';
-import { rerender } from '../../../test_helpers';
+import { rerender, getPageHeaderActions } from '../../../test_helpers';
-import { SynonymCard, SynonymModal, EmptyState } from './components';
+import { SynonymCard, SynonymModal } from './components';
import { Synonyms } from './';
@@ -53,21 +52,9 @@ describe('Synonyms', () => {
});
it('renders a create action button', () => {
- const wrapper = shallow( )
- .find(EuiPageHeader)
- .dive()
- .children()
- .dive();
-
- wrapper.find(EuiButton).simulate('click');
- expect(actions.openModal).toHaveBeenCalled();
- });
-
- it('renders an empty state if no synonyms exist', () => {
- setMockValues({ ...values, synonymSets: [] });
const wrapper = shallow( );
-
- expect(wrapper.find(EmptyState)).toHaveLength(1);
+ getPageHeaderActions(wrapper).find(EuiButton).simulate('click');
+ expect(actions.openModal).toHaveBeenCalled();
});
describe('loading', () => {
@@ -75,14 +62,14 @@ describe('Synonyms', () => {
setMockValues({ ...values, synonymSets: [], dataLoading: true });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(true);
});
it('does not render a full loading state after initial page load', () => {
setMockValues({ ...values, synonymSets: [MOCK_SYNONYM_SET], dataLoading: true });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(0);
+ expect(wrapper.prop('isLoading')).toEqual(false);
});
});
@@ -108,7 +95,7 @@ describe('Synonyms', () => {
const wrapper = shallow( );
expect(actions.onPaginate).not.toHaveBeenCalled();
- expect(wrapper.find(EmptyState)).toHaveLength(1);
+ expect(wrapper.prop('isEmptyState')).toEqual(true);
});
it('handles off-by-one shenanigans between EuiPagination and our API', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx
index d3ba53819f7de..4a68bc381f764 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx
@@ -9,21 +9,11 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import {
- EuiPageHeader,
- EuiButton,
- EuiPageContentBody,
- EuiSpacer,
- EuiFlexGrid,
- EuiFlexItem,
- EuiPagination,
-} from '@elastic/eui';
+import { EuiButton, EuiSpacer, EuiFlexGrid, EuiFlexItem, EuiPagination } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { SynonymCard, SynonymModal, EmptyState } from './components';
import { SYNONYMS_TITLE } from './constants';
@@ -46,46 +36,45 @@ export const Synonyms: React.FC = () => {
}
}, [synonymSets]);
- if (dataLoading && !hasSynonyms) return ;
-
return (
- <>
-
- openModal(null)}>
+ openModal(null)}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.synonyms.createSynonymSetButtonLabel',
{ defaultMessage: 'Create a synonym set' }
)}
,
- ]}
+ ],
+ }}
+ isLoading={dataLoading && !hasSynonyms}
+ isEmptyState={!hasSynonyms}
+ emptyState={ }
+ >
+
+ {synonymSets.map(({ id, synonyms }) => (
+
+
+
+ ))}
+
+
+ onPaginate(pageIndex + 1)}
/>
-
-
-
- {hasSynonyms ? (
- <>
-
- {synonymSets.map(({ id, synonyms }) => (
-
-
-
- ))}
-
-
- onPaginate(pageIndex + 1)}
- />
- >
- ) : (
-
- )}
-
-
- >
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
index 7b3b13aef05d6..191758af26758 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
@@ -104,9 +104,6 @@ export const AppSearchConfigured: React.FC> = (props) =
-
-
-
{canManageEngines && (
@@ -117,6 +114,9 @@ export const AppSearchConfigured: React.FC> = (props) =
)}
+
+
+
{canViewSettings && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
index bd5bdb7b2f665..d9d1935c648f7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
@@ -18,7 +18,7 @@ export const CREDENTIALS_PATH = '/credentials';
export const ROLE_MAPPINGS_PATH = '/role_mappings';
export const ENGINES_PATH = '/engines';
-export const ENGINE_CREATION_PATH = '/engine_creation';
+export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; // This is safe from conflicting with an :engineName path because new is a reserved name
export const ENGINE_PATH = `${ENGINES_PATH}/:engineName`;
export const ENGINE_ANALYTICS_PATH = `${ENGINE_PATH}/analytics`;
@@ -39,7 +39,7 @@ export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_SCHEMA_PATH}/reindex_job/:reind
export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`;
export const ENGINE_CRAWLER_DOMAIN_PATH = `${ENGINE_CRAWLER_PATH}/domains/:domainId`;
-export const META_ENGINE_CREATION_PATH = '/meta_engine_creation';
+export const META_ENGINE_CREATION_PATH = `${ENGINES_PATH}/new_meta_engine`; // This is safe from conflicting with an :engineName path because engine names cannot have underscores
export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`;
export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/relevance_tuning`;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts
index 70990727b8a62..b15bd9e1155cc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts
@@ -6,4 +6,5 @@
*/
export * from './actions';
+export * from './labels';
export { DEFAULT_META } from './default_meta';
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
similarity index 50%
rename from x-pack/plugins/security_solution/public/common/components/header_global/translations.ts
rename to x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
index a2a22dfe31eb9..8e6159d2b5b2a 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
@@ -7,13 +7,9 @@
import { i18n } from '@kbn/i18n';
-export const SECURITY_SOLUTION = i18n.translate(
- 'xpack.securitySolution.headerGlobal.securitySolution',
- {
- defaultMessage: 'Security solution',
- }
-);
-
-export const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.headerGlobal.buttonAddData', {
- defaultMessage: 'Add data',
+export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.usernameLabel', {
+ defaultMessage: 'Username',
+});
+export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.emailLabel', {
+ defaultMessage: 'Email',
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts
index b51416ac76ca7..8cfca3bade993 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts
@@ -19,21 +19,23 @@ import { generateNavLink, getNavLinkActive } from './nav_link_helpers';
describe('generateNavLink', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockKibanaValues.history.location.pathname = '/current_page';
+ mockKibanaValues.history.location.pathname = '/';
});
- it('generates React Router props & isSelected (active) state for use within an EuiSideNavItem obj', () => {
+ it('generates React Router props for use within an EuiSideNavItem obj', () => {
const navItem = generateNavLink({ to: '/test' });
- expect(navItem.href).toEqual('/app/enterprise_search/test');
+ expect(navItem).toEqual({
+ href: '/app/enterprise_search/test',
+ onClick: expect.any(Function),
+ isSelected: false,
+ });
navItem.onClick({} as any);
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test');
-
- expect(navItem.isSelected).toEqual(false);
});
- describe('getNavLinkActive', () => {
+ describe('isSelected / getNavLinkActive', () => {
it('returns true when the current path matches the link path', () => {
mockKibanaValues.history.location.pathname = '/test';
const isSelected = getNavLinkActive({ to: '/test' });
@@ -41,6 +43,13 @@ describe('generateNavLink', () => {
expect(isSelected).toEqual(true);
});
+ it('return false when the current path does not match the link path', () => {
+ mockKibanaValues.history.location.pathname = '/hello';
+ const isSelected = getNavLinkActive({ to: '/world' });
+
+ expect(isSelected).toEqual(false);
+ });
+
describe('isRoot', () => {
it('returns true if the current path is "/"', () => {
mockKibanaValues.history.location.pathname = '/';
@@ -58,7 +67,31 @@ describe('generateNavLink', () => {
expect(isSelected).toEqual(true);
});
- it('returns false if not', () => {
+ /* NOTE: This logic is primarily used for the following routing scenario:
+ * 1. /item/{itemId} shows a child subnav, e.g. /items/{itemId}/settings
+ * - BUT when the child subnav is open, the parent `Item` nav link should not show as active - its child nav links should
+ * 2. /item/create_item (example) does *not* show a child subnav
+ * - BUT the parent `Item` nav link should highlight when on this non-subnav route
+ */
+ it('returns false if subroutes already have their own items subnav (with active state)', () => {
+ mockKibanaValues.history.location.pathname = '/items/123/settings';
+ const isSelected = getNavLinkActive({
+ to: '/items',
+ shouldShowActiveForSubroutes: true,
+ items: [{ id: 'settings', name: 'Settings' }],
+ });
+
+ expect(isSelected).toEqual(false);
+ });
+
+ it('returns false if not a valid subroute', () => {
+ mockKibanaValues.history.location.pathname = '/hello/world';
+ const isSelected = getNavLinkActive({ to: '/world', shouldShowActiveForSubroutes: true });
+
+ expect(isSelected).toEqual(false);
+ });
+
+ it('returns false for subroutes if the flag is not passed', () => {
mockKibanaValues.history.location.pathname = '/hello/world';
const isSelected = getNavLinkActive({ to: '/hello' });
@@ -66,4 +99,10 @@ describe('generateNavLink', () => {
});
});
});
+
+ it('optionally passes items', () => {
+ const navItem = generateNavLink({ to: '/test', items: [] });
+
+ expect(navItem.items).toEqual([]);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts
index 6124636af3f99..9caf58886c52e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { EuiSideNavItemType } from '@elastic/eui';
+
import { stripTrailingSlash } from '../../../../common/strip_slashes';
import { KibanaLogic } from '../kibana';
@@ -14,12 +16,14 @@ interface Params {
to: string;
isRoot?: boolean;
shouldShowActiveForSubroutes?: boolean;
+ items?: Array>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper
}
-export const generateNavLink = ({ to, ...rest }: Params & ReactRouterProps) => {
+export const generateNavLink = ({ to, items, ...rest }: Params & ReactRouterProps) => {
return {
...generateReactRouterProps({ to, ...rest }),
- isSelected: getNavLinkActive({ to, ...rest }),
+ isSelected: getNavLinkActive({ to, items, ...rest }),
+ items,
};
};
@@ -27,14 +31,19 @@ export const getNavLinkActive = ({
to,
isRoot = false,
shouldShowActiveForSubroutes = false,
+ items = [],
}: Params): boolean => {
const { pathname } = KibanaLogic.values.history.location;
const currentPath = stripTrailingSlash(pathname);
- const isActive =
- currentPath === to ||
- (shouldShowActiveForSubroutes && currentPath.startsWith(to)) ||
- (isRoot && currentPath === '');
+ if (currentPath === to) return true;
+
+ if (isRoot && currentPath === '') return true;
+
+ if (shouldShowActiveForSubroutes) {
+ if (items.length) return false; // If a nav link has sub-nav items open, never show it as active
+ if (currentPath.startsWith(to)) return true;
+ }
- return isActive;
+ return false;
};
diff --git a/x-pack/plugins/ml/common/constants/embeddable_map.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
similarity index 66%
rename from x-pack/plugins/ml/common/constants/embeddable_map.ts
rename to x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
index 6cb345bae630e..500f560675679 100644
--- a/x-pack/plugins/ml/common/constants/embeddable_map.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-export const COMMON_EMS_LAYER_IDS = [
- 'world_countries',
- 'administrative_regions_lvl2',
- 'usa_zip_codes',
- 'usa_states',
+export const elasticsearchUsers = [
+ {
+ email: 'user1@user.com',
+ username: 'user1',
+ },
];
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts
index 15dec753351ba..486c1ba6c9af6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts
@@ -9,6 +9,8 @@ import { engines } from '../../../app_search/__mocks__/engines.mock';
import { AttributeName } from '../../types';
+import { elasticsearchUsers } from './elasticsearch_users';
+
export const asRoleMapping = {
id: 'sdgfasdgadf123',
attributeName: 'role' as AttributeName,
@@ -70,3 +72,20 @@ export const wsRoleMapping = {
},
],
};
+
+export const invitation = {
+ email: 'foo@example.com',
+ code: '123fooqwe',
+};
+
+export const wsSingleUserRoleMapping = {
+ invitation,
+ elasticsearchUser: elasticsearchUsers[0],
+ roleMapping: wsRoleMapping,
+};
+
+export const asSingleUserRoleMapping = {
+ invitation,
+ elasticsearchUser: elasticsearchUsers[0],
+ roleMapping: asRoleMapping,
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts
index 9f40844e52470..45cab32b67e08 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts
@@ -50,10 +50,26 @@ export const ROLE_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.rol
defaultMessage: 'Role',
});
+export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.usernameLabel', {
+ defaultMessage: 'Username',
+});
+
+export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.emailLabel', {
+ defaultMessage: 'Email',
+});
+
export const ALL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.allLabel', {
defaultMessage: 'All',
});
+export const GROUPS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.groupsLabel', {
+ defaultMessage: 'Groups',
+});
+
+export const ENGINES_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.enginesLabel', {
+ defaultMessage: 'Engines',
+});
+
export const AUTH_PROVIDER_LABEL = i18n.translate(
'xpack.enterpriseSearch.roleMapping.authProviderLabel',
{
@@ -82,10 +98,10 @@ export const ATTRIBUTE_VALUE_ERROR = i18n.translate(
}
);
-export const DELETE_ROLE_MAPPING_TITLE = i18n.translate(
- 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle',
+export const REMOVE_ROLE_MAPPING_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle',
{
- defaultMessage: 'Remove this role mapping',
+ defaultMessage: 'Remove role mapping',
}
);
@@ -96,10 +112,17 @@ export const DELETE_ROLE_MAPPING_DESCRIPTION = i18n.translate(
}
);
-export const DELETE_ROLE_MAPPING_BUTTON = i18n.translate(
- 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton',
+export const REMOVE_ROLE_MAPPING_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.removeRoleMappingButton',
+ {
+ defaultMessage: 'Remove mapping',
+ }
+);
+
+export const REMOVE_USER_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.removeUserButton',
{
- defaultMessage: 'Delete mapping',
+ defaultMessage: 'Remove user',
}
);
@@ -205,3 +228,181 @@ export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.roleMapping.noResults.message',
{ defaultMessage: 'Create a new role mapping' }
);
+
+export const ROLES_DISABLED_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.rolesDisabledTitle',
+ { defaultMessage: 'Role-based access is disabled' }
+);
+
+export const ROLES_DISABLED_DESCRIPTION = (productName: ProductName) =>
+ i18n.translate('xpack.enterpriseSearch.roleMapping.rolesDisabledDescription', {
+ defaultMessage:
+ 'All users set for this deployment currently have full access to {productName}. To restrict access and manage permissions, you must enable role-based access for Enterprise Search.',
+ values: { productName },
+ });
+
+export const ROLES_DISABLED_NOTE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.rolesDisabledNote',
+ {
+ defaultMessage:
+ 'Note: enabling role-based access restricts access for both App Search and Workplace Search. Once enabled, review access management for both products, if applicable.',
+ }
+);
+
+export const ENABLE_ROLES_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.enableRolesButton',
+ { defaultMessage: 'Enable role-based access' }
+);
+
+export const ENABLE_ROLES_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.enableRolesLink',
+ { defaultMessage: 'Learn more about role-based access' }
+);
+
+export const INVITATION_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.invitationDescription',
+ {
+ defaultMessage:
+ 'This URL can be shared with the user, allowing them to accept the Enterprise Search invitation and set a new password',
+ }
+);
+
+export const NEW_INVITATION_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.newInvitationLabel',
+ { defaultMessage: 'Invitation URL' }
+);
+
+export const EXISTING_INVITATION_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.existingInvitationLabel',
+ { defaultMessage: 'The user has not yet accepted the invitation.' }
+);
+
+export const INVITATION_LINK = i18n.translate('xpack.enterpriseSearch.roleMapping.invitationLink', {
+ defaultMessage: 'Enterprise Search Invitation Link',
+});
+
+export const NO_USERS_TITLE = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersTitle', {
+ defaultMessage: 'No user added',
+});
+
+export const NO_USERS_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.noUsersDescription',
+ {
+ defaultMessage:
+ 'Users can be added individually, for flexibility. Role mappings provide a broader interface for adding large number of users using user attributes.',
+ }
+);
+
+export const ENABLE_USERS_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.enableUsersLink',
+ { defaultMessage: 'Learn more about user management' }
+);
+
+export const NEW_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.newUserLabel', {
+ defaultMessage: 'Create new user',
+});
+
+export const EXISTING_USER_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.existingUserLabel',
+ { defaultMessage: 'Add existing user' }
+);
+
+export const USERNAME_NO_USERS_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usernameNoUsersText',
+ { defaultMessage: 'No existing user eligible for addition.' }
+);
+
+export const REQUIRED_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.requiredLabel', {
+ defaultMessage: 'Required',
+});
+
+export const USERS_HEADING_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usersHeadingTitle',
+ { defaultMessage: 'Users' }
+);
+
+export const USERS_HEADING_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usersHeadingDescription',
+ {
+ defaultMessage:
+ 'User management provides granular access for individual or special permission needs. Users from federated sources such as SAML are managed by role mappings, and excluded from this list.',
+ }
+);
+
+export const USERS_HEADING_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usersHeadingLabel',
+ { defaultMessage: 'Add a new user' }
+);
+
+export const UPDATE_USER_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.updateUserLabel',
+ {
+ defaultMessage: 'Update user',
+ }
+);
+
+export const ADD_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.addUserLabel', {
+ defaultMessage: 'Add user',
+});
+
+export const USER_ADDED_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.userAddedLabel',
+ {
+ defaultMessage: 'User added',
+ }
+);
+
+export const USER_UPDATED_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.userUpdatedLabel',
+ {
+ defaultMessage: 'User updated',
+ }
+);
+
+export const NEW_USER_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.newUserDescription',
+ {
+ defaultMessage: 'Provide granular access and permissions',
+ }
+);
+
+export const UPDATE_USER_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.updateUserDescription',
+ {
+ defaultMessage: 'Manage granular access and permissions',
+ }
+);
+
+export const INVITATION_PENDING_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.invitationPendingLabel',
+ {
+ defaultMessage: 'Invitation pending',
+ }
+);
+
+export const ROLE_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.roleModalText', {
+ defaultMessage:
+ 'Removing a role mapping revokes access to any user corresponding to the mapping attributes, but may not take effect immediately for SAML-governed roles. Users with an active SAML session will retain access until it expires.',
+});
+
+export const USER_MODAL_TITLE = (username: string) =>
+ i18n.translate('xpack.enterpriseSearch.roleMapping.userModalTitle', {
+ defaultMessage: 'Remove {username}',
+ values: { username },
+ });
+
+export const USER_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.userModalText', {
+ defaultMessage:
+ 'Removing a user immediately revokes access to the experience, unless this user’s attributes also corresponds to a role mapping for native and SAML-governed authentication, in which case associated role mappings should also be reviewed and adjusted, as needed.',
+});
+
+export const FILTER_USERS_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.filterUsersLabel',
+ {
+ defaultMessage: 'Filter users',
+ }
+);
+
+export const NO_USERS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersLabel', {
+ defaultMessage: 'No matching users found',
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
index b0d10e9692714..8096b86939ff3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
@@ -6,9 +6,17 @@
*/
export { AttributeSelector } from './attribute_selector';
+export { RolesEmptyPrompt } from './roles_empty_prompt';
export { RoleMappingsTable } from './role_mappings_table';
export { RoleOptionLabel } from './role_option_label';
export { RoleSelector } from './role_selector';
export { RoleMappingFlyout } from './role_mapping_flyout';
export { RoleMappingsHeading } from './role_mappings_heading';
+export { UserAddedInfo } from './user_added_info';
+export { UserFlyout } from './user_flyout';
+export { UsersHeading } from './users_heading';
+export { UserInvitationCallout } from './user_invitation_callout';
+export { UserSelector } from './user_selector';
+export { UsersTable } from './users_table';
export { UsersAndRolesRowActions } from './users_and_roles_row_actions';
+export { UsersEmptyPrompt } from './users_empty_prompt';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx
index f0bf86fb306c6..5a2958d60dc2c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx
@@ -15,7 +15,13 @@ import { RoleMappingsHeading } from './role_mappings_heading';
describe('RoleMappingsHeading', () => {
it('renders ', () => {
- const wrapper = shallow( );
+ const wrapper = shallow(
+
+ );
expect(wrapper.find(EuiTitle)).toHaveLength(1);
expect(wrapper.find(EuiText)).toHaveLength(1);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx
index eee8b180d3281..1984cc6c60a34 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx
@@ -28,13 +28,11 @@ import {
interface Props {
productName: ProductName;
+ docsLink: string;
onClick(): void;
}
-// TODO: Replace EuiLink href with acutal docs link when available
-const ROLE_MAPPINGS_DOCS_HREF = '#TODO';
-
-export const RoleMappingsHeading: React.FC = ({ productName, onClick }) => (
+export const RoleMappingsHeading: React.FC = ({ productName, docsLink, onClick }) => (
@@ -45,7 +43,7 @@ export const RoleMappingsHeading: React.FC = ({ productName, onClick }) =
{ROLE_MAPPINGS_HEADING_DESCRIPTION(productName)}{' '}
-
+
{ROLE_MAPPINGS_HEADING_DOCS_LINK}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx
index 156b52a4016c3..81a7c06020165 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx
@@ -13,7 +13,9 @@ import { mount } from 'enzyme';
import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui';
-import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants';
+import { engines } from '../../app_search/__mocks__/engines.mock';
+
+import { ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants';
import { RoleMappingsTable } from './role_mappings_table';
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
@@ -78,28 +80,30 @@ describe('RoleMappingsTable', () => {
expect(handleDeleteMapping).toHaveBeenCalled();
});
- it('shows default message when "accessAllEngines" is true', () => {
+ it('handles access items display for all items', () => {
const wrapper = mount(
);
- expect(wrapper.find('[data-test-subj="AccessItemsList"]').prop('children')).toEqual(ALL_LABEL);
+ expect(wrapper.find('[data-test-subj="AllItems"]')).toHaveLength(1);
});
- it('handles display when no items present', () => {
- const noItemsRoleMapping = { ...asRoleMapping, engines: [] };
- noItemsRoleMapping.accessAllEngines = false;
-
+ it('handles access items display more than 2 items', () => {
+ const extraEngine = {
+ ...engines[0],
+ id: '3',
+ };
+
+ const roleMapping = {
+ ...asRoleMapping,
+ engines: [...engines, extraEngine],
+ accessAllEngines: false,
+ };
const wrapper = mount(
-
+
);
-
- expect(wrapper.find('[data-test-subj="AccessItemsList"]').children().children().text()).toEqual(
- '—'
+ expect(wrapper.find('[data-test-subj="AccessItems"]').prop('children')).toEqual(
+ `${engines[0].name}, ${engines[1].name} + 1`
);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
index 7696cf03ed4b1..eb9621c7a242c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
@@ -5,9 +5,9 @@
* 2.0.
*/
-import React, { Fragment } from 'react';
+import React from 'react';
-import { EuiIconTip, EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
+import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
import { ASRoleMapping } from '../../app_search/types';
import { WSRoleMapping } from '../../workplace_search/types';
@@ -46,8 +46,6 @@ interface Props {
handleDeleteMapping(roleMappingId: string): void;
}
-const noItemsPlaceholder = — ;
-
const getAuthProviderDisplayValue = (authProvider: string) =>
authProvider === ANY_AUTH_PROVIDER ? ANY_AUTH_PROVIDER_OPTION_LABEL : authProvider;
@@ -90,24 +88,18 @@ export const RoleMappingsTable: React.FC = ({
const accessItemsCol: EuiBasicTableColumn = {
field: 'accessItems',
name: accessHeader,
- render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => (
-
- {accessAllEngines ? (
- ALL_LABEL
- ) : (
- <>
- {accessItems.length === 0
- ? noItemsPlaceholder
- : accessItems.map(({ name }) => (
-
- {name}
-
-
- ))}
- >
- )}
-
- ),
+ render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => {
+ // Design calls for showing the first 2 items followed by a +x after those 2.
+ // ['foo', 'bar', 'baz'] would display as: "foo, bar + 1"
+ const numItems = accessItems.length;
+ if (accessAllEngines || numItems === 0)
+ return {ALL_LABEL} ;
+ const additionalItems = numItems > 2 ? ` + ${numItems - 2}` : '';
+ const names = accessItems.map((item) => item.name);
+ return (
+ {names.slice(0, 2).join(', ') + additionalItems}
+ );
+ },
};
const authProviderCol: EuiBasicTableColumn = {
@@ -143,6 +135,7 @@ export const RoleMappingsTable: React.FC = ({
const pagination = {
hidePerPageOptions: true,
+ pageSize: 10,
};
const search = {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx
new file mode 100644
index 0000000000000..8331a45849e3a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiButton, EuiLink, EuiEmptyPrompt } from '@elastic/eui';
+
+import { RolesEmptyPrompt } from './roles_empty_prompt';
+
+describe('RolesEmptyPrompt', () => {
+ const onEnable = jest.fn();
+
+ const props = {
+ productName: 'App Search',
+ docsLink: 'http://elastic.co',
+ onEnable,
+ };
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ expect(wrapper.find(EuiEmptyPrompt).dive().find(EuiLink).prop('href')).toEqual(props.docsLink);
+ });
+
+ it('calls onEnable on change', () => {
+ const wrapper = shallow( );
+ const prompt = wrapper.find(EuiEmptyPrompt).dive();
+ prompt.find(EuiButton).simulate('click');
+
+ expect(onEnable).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.tsx
new file mode 100644
index 0000000000000..11d50573c45f6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiEmptyPrompt, EuiButton, EuiLink, EuiSpacer } from '@elastic/eui';
+
+import { ProductName } from '../types';
+
+import {
+ ROLES_DISABLED_TITLE,
+ ROLES_DISABLED_DESCRIPTION,
+ ROLES_DISABLED_NOTE,
+ ENABLE_ROLES_BUTTON,
+ ENABLE_ROLES_LINK,
+} from './constants';
+
+interface Props {
+ productName: ProductName;
+ docsLink: string;
+ onEnable(): void;
+}
+
+export const RolesEmptyPrompt: React.FC = ({ onEnable, docsLink, productName }) => (
+ {ROLES_DISABLED_TITLE}}
+ body={
+ <>
+ {ROLES_DISABLED_DESCRIPTION(productName)}
+ {ROLES_DISABLED_NOTE}
+ >
+ }
+ actions={[
+
+ {ENABLE_ROLES_BUTTON}
+ ,
+ ,
+
+ {ENABLE_ROLES_LINK}
+ ,
+ ]}
+ />
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx
new file mode 100644
index 0000000000000..30bdaa0010b58
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiText } from '@elastic/eui';
+
+import { UserAddedInfo } from './';
+
+describe('UserAddedInfo', () => {
+ const props = {
+ username: 'user1',
+ email: 'test@test.com',
+ roleType: 'user',
+ };
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiText)).toHaveLength(6);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx
new file mode 100644
index 0000000000000..a12eae66262a0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiSpacer, EuiText } from '@elastic/eui';
+
+import { USERNAME_LABEL, EMAIL_LABEL } from '../constants';
+
+import { ROLE_LABEL } from './constants';
+
+interface Props {
+ username: string;
+ email: string;
+ roleType: string;
+}
+
+export const UserAddedInfo: React.FC = ({ username, email, roleType }) => (
+ <>
+
+ {USERNAME_LABEL}
+
+ {username}
+
+
+ {EMAIL_LABEL}
+
+ {email}
+
+
+ {ROLE_LABEL}
+
+ {roleType}
+
+ >
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx
new file mode 100644
index 0000000000000..43333fe048f23
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiFlyout, EuiText, EuiIcon } from '@elastic/eui';
+
+import {
+ USERS_HEADING_LABEL,
+ UPDATE_USER_LABEL,
+ USER_UPDATED_LABEL,
+ NEW_USER_DESCRIPTION,
+ UPDATE_USER_DESCRIPTION,
+} from './constants';
+
+import { UserFlyout } from './';
+
+describe('UserFlyout', () => {
+ const closeUserFlyout = jest.fn();
+ const handleSaveUser = jest.fn();
+
+ const props = {
+ children:
,
+ isNew: true,
+ isComplete: false,
+ disabled: false,
+ closeUserFlyout,
+ handleSaveUser,
+ };
+
+ it('renders for new user', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiFlyout)).toHaveLength(1);
+ expect(wrapper.find('h2').prop('children')).toEqual(USERS_HEADING_LABEL);
+ expect(wrapper.find(EuiText).prop('children')).toEqual({NEW_USER_DESCRIPTION}
);
+ });
+
+ it('renders for existing user', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find('h2').prop('children')).toEqual(UPDATE_USER_LABEL);
+ expect(wrapper.find(EuiText).prop('children')).toEqual({UPDATE_USER_DESCRIPTION}
);
+ });
+
+ it('renders icon and message for completed user', () => {
+ const wrapper = shallow( );
+ const icon = (
+
+ );
+ const children = (
+
+ {USER_UPDATED_LABEL} {icon}
+
+ );
+
+ expect(wrapper.find('h2').prop('children')).toEqual(children);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx
new file mode 100644
index 0000000000000..e13a56a716929
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiIcon,
+ EuiText,
+ EuiTitle,
+ EuiSpacer,
+} from '@elastic/eui';
+
+interface Props {
+ children: React.ReactNode;
+ isNew: boolean;
+ isComplete: boolean;
+ disabled: boolean;
+ closeUserFlyout(): void;
+ handleSaveUser(): void;
+}
+
+import { CANCEL_BUTTON_LABEL, CLOSE_BUTTON_LABEL } from '../constants';
+
+import {
+ USERS_HEADING_LABEL,
+ UPDATE_USER_LABEL,
+ ADD_USER_LABEL,
+ USER_ADDED_LABEL,
+ USER_UPDATED_LABEL,
+ NEW_USER_DESCRIPTION,
+ UPDATE_USER_DESCRIPTION,
+} from './constants';
+
+export const UserFlyout: React.FC = ({
+ children,
+ isNew,
+ isComplete,
+ disabled,
+ closeUserFlyout,
+ handleSaveUser,
+}) => {
+ const savedIcon = (
+
+ );
+ const IS_EDITING_HEADING = isNew ? USERS_HEADING_LABEL : UPDATE_USER_LABEL;
+ const IS_EDITING_DESCRIPTION = isNew ? NEW_USER_DESCRIPTION : UPDATE_USER_DESCRIPTION;
+ const USER_SAVED_HEADING = isNew ? USER_ADDED_LABEL : USER_UPDATED_LABEL;
+ const IS_COMPLETE_HEADING = (
+
+ {USER_SAVED_HEADING} {savedIcon}
+
+ );
+
+ const editingFooterActions = (
+
+
+ {CANCEL_BUTTON_LABEL}
+
+
+
+ {isNew ? ADD_USER_LABEL : UPDATE_USER_LABEL}
+
+
+
+ );
+
+ const completedFooterAction = (
+
+
+
+ {CLOSE_BUTTON_LABEL}
+
+
+
+ );
+
+ return (
+
+
+
+ {isComplete ? IS_COMPLETE_HEADING : IS_EDITING_HEADING}
+
+ {!isComplete && (
+
+ {IS_EDITING_DESCRIPTION}
+
+ )}
+
+
+ {children}
+
+
+ {isComplete ? completedFooterAction : editingFooterActions}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx
new file mode 100644
index 0000000000000..d5272a26715b6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiText, EuiButtonIcon, EuiCopy } from '@elastic/eui';
+
+import { EXISTING_INVITATION_LABEL } from './constants';
+
+import { UserInvitationCallout } from './';
+
+describe('UserInvitationCallout', () => {
+ const props = {
+ isNew: true,
+ invitationCode: 'test@test.com',
+ urlPrefix: 'http://foo',
+ };
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiText)).toHaveLength(2);
+ });
+
+ it('renders the copy button', () => {
+ const copyMock = jest.fn();
+ const wrapper = shallow( );
+
+ const copyEl = shallow({wrapper.find(EuiCopy).props().children(copyMock)}
);
+ expect(copyEl.find(EuiButtonIcon).props().onClick).toEqual(copyMock);
+ });
+
+ it('renders existing invitation label', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiText).first().prop('children')).toEqual(
+ {EXISTING_INVITATION_LABEL}
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx
new file mode 100644
index 0000000000000..8310077ad6f2e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiCopy, EuiButtonIcon, EuiSpacer, EuiText, EuiLink } from '@elastic/eui';
+
+import {
+ INVITATION_DESCRIPTION,
+ NEW_INVITATION_LABEL,
+ EXISTING_INVITATION_LABEL,
+ INVITATION_LINK,
+} from './constants';
+
+interface Props {
+ isNew: boolean;
+ invitationCode: string;
+ urlPrefix: string;
+}
+
+export const UserInvitationCallout: React.FC = ({ isNew, invitationCode, urlPrefix }) => {
+ const link = urlPrefix + invitationCode;
+ const label = isNew ? NEW_INVITATION_LABEL : EXISTING_INVITATION_LABEL;
+
+ return (
+ <>
+ {!isNew && }
+
+ {label}
+
+
+ {INVITATION_DESCRIPTION}
+
+
+ {INVITATION_LINK}
+ {' '}
+
+ {(copy) => }
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx
new file mode 100644
index 0000000000000..08ddc7ba5427f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { elasticsearchUsers } from './__mocks__/elasticsearch_users';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiFormRow } from '@elastic/eui';
+
+import { Role as ASRole } from '../../app_search/types';
+
+import { REQUIRED_LABEL, USERNAME_NO_USERS_TEXT } from './constants';
+
+import { UserSelector } from './';
+
+const simulatedEvent = {
+ target: { value: 'foo' },
+};
+
+describe('UserSelector', () => {
+ const setUserExisting = jest.fn();
+ const setElasticsearchUsernameValue = jest.fn();
+ const setElasticsearchEmailValue = jest.fn();
+ const handleRoleChange = jest.fn();
+ const handleUsernameSelectChange = jest.fn();
+
+ const roleType = ('user' as unknown) as ASRole;
+
+ const props = {
+ isNewUser: true,
+ userFormUserIsExisting: true,
+ elasticsearchUsers,
+ elasticsearchUser: elasticsearchUsers[0],
+ roleTypes: [roleType],
+ roleType,
+ setUserExisting,
+ setElasticsearchUsernameValue,
+ setElasticsearchEmailValue,
+ handleRoleChange,
+ handleUsernameSelectChange,
+ };
+
+ it('renders Role select and calls method', () => {
+ const wrapper = shallow( );
+ wrapper.find('[data-test-subj="RoleSelect"]').simulate('change', simulatedEvent);
+
+ expect(handleRoleChange).toHaveBeenCalled();
+ });
+
+ it('renders when updating user', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find('[data-test-subj="UsernameInput"]')).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="EmailInput"]')).toHaveLength(1);
+ });
+
+ it('renders Username select and calls method', () => {
+ const wrapper = shallow( );
+ wrapper.find('[data-test-subj="UsernameSelect"]').simulate('change', simulatedEvent);
+
+ expect(handleUsernameSelectChange).toHaveBeenCalled();
+ });
+
+ it('renders Existing user radio and calls method', () => {
+ const wrapper = shallow( );
+ wrapper.find('[data-test-subj="ExistingUserRadio"]').simulate('change');
+
+ expect(setUserExisting).toHaveBeenCalledWith(true);
+ });
+
+ it('renders Email input and calls method', () => {
+ const wrapper = shallow( );
+ wrapper.find('[data-test-subj="EmailInput"]').simulate('change', simulatedEvent);
+
+ expect(setElasticsearchEmailValue).toHaveBeenCalled();
+ });
+
+ it('renders Username input and calls method', () => {
+ const wrapper = shallow( );
+ wrapper.find('[data-test-subj="UsernameInput"]').simulate('change', simulatedEvent);
+
+ expect(setElasticsearchUsernameValue).toHaveBeenCalled();
+ });
+
+ it('renders New user radio and calls method', () => {
+ const wrapper = shallow( );
+ wrapper.find('[data-test-subj="NewUserRadio"]').simulate('change');
+
+ expect(setUserExisting).toHaveBeenCalledWith(false);
+ });
+
+ it('renders helpText when values are empty', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.find(EuiFormRow).at(0).prop('helpText')).toEqual(USERNAME_NO_USERS_TEXT);
+ expect(wrapper.find(EuiFormRow).at(1).prop('helpText')).toEqual(REQUIRED_LABEL);
+ expect(wrapper.find(EuiFormRow).at(2).prop('helpText')).toEqual(REQUIRED_LABEL);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx
new file mode 100644
index 0000000000000..70348bf29894a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import {
+ EuiFieldText,
+ EuiRadio,
+ EuiFormRow,
+ EuiSelect,
+ EuiSelectOption,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { Role as ASRole } from '../../app_search/types';
+import { ElasticsearchUser } from '../../shared/types';
+import { Role as WSRole } from '../../workplace_search/types';
+
+import { USERNAME_LABEL, EMAIL_LABEL } from '../constants';
+
+import {
+ NEW_USER_LABEL,
+ EXISTING_USER_LABEL,
+ USERNAME_NO_USERS_TEXT,
+ REQUIRED_LABEL,
+ ROLE_LABEL,
+} from './constants';
+
+type SharedRole = WSRole | ASRole;
+
+interface Props {
+ isNewUser: boolean;
+ userFormUserIsExisting: boolean;
+ elasticsearchUsers: ElasticsearchUser[];
+ elasticsearchUser: ElasticsearchUser;
+ roleTypes: SharedRole[];
+ roleType: SharedRole;
+ setUserExisting(userFormUserIsExisting: boolean): void;
+ setElasticsearchUsernameValue(username: string): void;
+ setElasticsearchEmailValue(email: string): void;
+ handleRoleChange(roleType: SharedRole): void;
+ handleUsernameSelectChange(username: string): void;
+}
+
+export const UserSelector: React.FC = ({
+ isNewUser,
+ userFormUserIsExisting,
+ elasticsearchUsers,
+ elasticsearchUser,
+ roleTypes,
+ roleType,
+ setUserExisting,
+ setElasticsearchUsernameValue,
+ setElasticsearchEmailValue,
+ handleRoleChange,
+ handleUsernameSelectChange,
+}) => {
+ const roleOptions = roleTypes.map((role) => ({ id: role, text: role }));
+ const usernameOptions = elasticsearchUsers.map(({ username }) => ({
+ id: username,
+ text: username,
+ }));
+ const hasElasticsearchUsers = elasticsearchUsers.length > 0;
+ const showNewUserExistingUserControls = userFormUserIsExisting && hasElasticsearchUsers;
+
+ const roleSelect = (
+
+ handleRoleChange(e.target.value as SharedRole)}
+ />
+
+ );
+
+ const emailInput = (
+
+ setElasticsearchEmailValue(e.target.value)}
+ />
+
+ );
+
+ const usernameAndEmailControls = (
+ <>
+
+ setElasticsearchUsernameValue(e.target.value)}
+ />
+
+ {elasticsearchUser.email !== null && emailInput}
+ {roleSelect}
+ >
+ );
+
+ const existingUserControls = (
+ <>
+
+
+ handleUsernameSelectChange(e.target.value)}
+ />
+
+ {roleSelect}
+ >
+ );
+
+ const newUserControls = (
+ <>
+
+ {usernameAndEmailControls}
+ >
+ );
+
+ const createUserControls = (
+ <>
+
+ setUserExisting(true)}
+ disabled={!hasElasticsearchUsers}
+ />
+
+
+ {showNewUserExistingUserControls && existingUserControls}
+
+ setUserExisting(false)}
+ />
+ {!showNewUserExistingUserControls && newUserControls}
+ >
+ );
+
+ return isNewUser ? createUserControls : usernameAndEmailControls;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx
index dbb47b50d4066..5f1fefc688c77 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx
@@ -9,15 +9,23 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiButtonIcon } from '@elastic/eui';
+import { EuiButtonIcon, EuiConfirmModal } from '@elastic/eui';
+
+import {
+ REMOVE_ROLE_MAPPING_TITLE,
+ REMOVE_ROLE_MAPPING_BUTTON,
+ ROLE_MODAL_TEXT,
+} from './constants';
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
describe('UsersAndRolesRowActions', () => {
const onManageClick = jest.fn();
const onDeleteClick = jest.fn();
+ const username = 'foo';
const props = {
+ username,
onManageClick,
onDeleteClick,
};
@@ -40,7 +48,19 @@ describe('UsersAndRolesRowActions', () => {
const wrapper = shallow( );
const button = wrapper.find(EuiButtonIcon).last();
button.simulate('click');
+ wrapper.find(EuiConfirmModal).prop('onConfirm')!({} as any);
expect(onDeleteClick).toHaveBeenCalled();
});
+
+ it('renders role mapping confirm modal text', () => {
+ const wrapper = shallow( );
+ const button = wrapper.find(EuiButtonIcon).last();
+ button.simulate('click');
+ const modal = wrapper.find(EuiConfirmModal);
+
+ expect(modal.prop('title')).toEqual(REMOVE_ROLE_MAPPING_TITLE);
+ expect(modal.prop('children')).toEqual({ROLE_MODAL_TEXT}
);
+ expect(modal.prop('confirmButtonText')).toEqual(REMOVE_ROLE_MAPPING_BUTTON);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx
index 3d956c0aabd68..a3b0d24769bf6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx
@@ -5,20 +5,65 @@
* 2.0.
*/
-import React from 'react';
+import React, { useState } from 'react';
-import { EuiButtonIcon } from '@elastic/eui';
+import { EuiButtonIcon, EuiConfirmModal } from '@elastic/eui';
-import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants';
+import { CANCEL_BUTTON_LABEL, MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants';
+
+import {
+ REMOVE_ROLE_MAPPING_TITLE,
+ REMOVE_ROLE_MAPPING_BUTTON,
+ REMOVE_USER_BUTTON,
+ ROLE_MODAL_TEXT,
+ USER_MODAL_TITLE,
+ USER_MODAL_TEXT,
+} from './constants';
interface Props {
+ username?: string;
onManageClick(): void;
onDeleteClick(): void;
}
-export const UsersAndRolesRowActions: React.FC = ({ onManageClick, onDeleteClick }) => (
- <>
- {' '}
-
- >
-);
+export const UsersAndRolesRowActions: React.FC = ({
+ onManageClick,
+ onDeleteClick,
+ username,
+}) => {
+ const [deleteModalVisible, setVisible] = useState(false);
+ const showDeleteModal = () => setVisible(true);
+ const closeDeleteModal = () => setVisible(false);
+ const title = username ? USER_MODAL_TITLE(username) : REMOVE_ROLE_MAPPING_TITLE;
+ const text = username ? USER_MODAL_TEXT : ROLE_MODAL_TEXT;
+ const confirmButton = username ? REMOVE_USER_BUTTON : REMOVE_ROLE_MAPPING_BUTTON;
+
+ const deleteModal = (
+ {
+ onDeleteClick();
+ closeDeleteModal();
+ }}
+ cancelButtonText={CANCEL_BUTTON_LABEL}
+ confirmButtonText={confirmButton}
+ buttonColor="danger"
+ defaultFocusedButton="confirm"
+ >
+ {text}
+
+ );
+
+ return (
+ <>
+ {deleteModalVisible && deleteModal}
+ {' '}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx
new file mode 100644
index 0000000000000..9110c09827c49
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiEmptyPrompt } from '@elastic/eui';
+
+import { UsersEmptyPrompt } from './';
+
+describe('UsersEmptyPrompt', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx
new file mode 100644
index 0000000000000..42bf690c388c4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiEmptyPrompt,
+ EuiLink,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { docLinks } from '../doc_links';
+
+import { NO_USERS_TITLE, NO_USERS_DESCRIPTION, ENABLE_USERS_LINK } from './constants';
+
+const USERS_DOCS_URL = `${docLinks.enterpriseSearchBase}/users-access.html`;
+
+export const UsersEmptyPrompt: React.FC = () => (
+
+
+
+
+ {NO_USERS_TITLE}}
+ body={{NO_USERS_DESCRIPTION}
}
+ actions={
+
+ {ENABLE_USERS_LINK}
+
+ }
+ />
+
+
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx
new file mode 100644
index 0000000000000..9bae93079e89f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiButton, EuiText, EuiTitle } from '@elastic/eui';
+
+import { UsersHeading } from './';
+
+describe('UsersHeading', () => {
+ const onClick = jest.fn();
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiText)).toHaveLength(1);
+ expect(wrapper.find(EuiTitle)).toHaveLength(1);
+ });
+
+ it('handles button click', () => {
+ const wrapper = shallow( );
+ wrapper.find(EuiButton).simulate('click');
+
+ expect(onClick).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx
new file mode 100644
index 0000000000000..8d097e21e9c3f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+
+import { USERS_HEADING_TITLE, USERS_HEADING_DESCRIPTION, USERS_HEADING_LABEL } from './constants';
+
+interface Props {
+ onClick(): void;
+}
+
+export const UsersHeading: React.FC = ({ onClick }) => (
+ <>
+
+
+
+ {USERS_HEADING_TITLE}
+
+
+ {USERS_HEADING_DESCRIPTION}
+
+
+
+
+ {USERS_HEADING_LABEL}
+
+
+
+
+ >
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx
new file mode 100644
index 0000000000000..dc1a2713ced12
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { asSingleUserRoleMapping, wsSingleUserRoleMapping, asRoleMapping } from './__mocks__/roles';
+
+import React from 'react';
+
+import { shallow, mount } from 'enzyme';
+
+import { EuiInMemoryTable, EuiTextColor } from '@elastic/eui';
+
+import { engines } from '../../app_search/__mocks__/engines.mock';
+
+import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
+
+import { UsersTable } from './';
+
+describe('UsersTable', () => {
+ const initializeSingleUserRoleMapping = jest.fn();
+ const handleDeleteMapping = jest.fn();
+ const props = {
+ accessItemKey: 'groups' as 'groups' | 'engines',
+ singleUserRoleMappings: [wsSingleUserRoleMapping],
+ initializeSingleUserRoleMapping,
+ handleDeleteMapping,
+ };
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
+ });
+
+ it('handles manage click', () => {
+ const wrapper = mount( );
+ wrapper.find(UsersAndRolesRowActions).prop('onManageClick')();
+
+ expect(initializeSingleUserRoleMapping).toHaveBeenCalled();
+ });
+
+ it('handles delete click', () => {
+ const wrapper = mount( );
+ wrapper.find(UsersAndRolesRowActions).prop('onDeleteClick')();
+
+ expect(handleDeleteMapping).toHaveBeenCalled();
+ });
+
+ it('handles display when no email present', () => {
+ const userWithNoEmail = {
+ ...wsSingleUserRoleMapping,
+ elasticsearchUser: {
+ email: null,
+ username: 'foo',
+ },
+ };
+ const wrapper = mount( );
+
+ expect(wrapper.find(EuiTextColor)).toHaveLength(1);
+ });
+
+ it('handles access items display for all items', () => {
+ const userWithAllItems = {
+ ...asSingleUserRoleMapping,
+ roleMapping: {
+ ...asRoleMapping,
+ engines: [],
+ },
+ };
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="AllItems"]')).toHaveLength(1);
+ });
+
+ it('handles access items display more than 2 items', () => {
+ const extraEngine = {
+ ...engines[0],
+ id: '3',
+ };
+ const userWithAllItems = {
+ ...asSingleUserRoleMapping,
+ roleMapping: {
+ ...asRoleMapping,
+ engines: [...engines, extraEngine],
+ },
+ };
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="AccessItems"]').prop('children')).toEqual(
+ `${engines[0].name}, ${engines[1].name} + 1`
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx
new file mode 100644
index 0000000000000..86dc2c2626229
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx
@@ -0,0 +1,147 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiBadge, EuiBasicTableColumn, EuiInMemoryTable, EuiTextColor } from '@elastic/eui';
+
+import { ASRoleMapping } from '../../app_search/types';
+import { SingleUserRoleMapping } from '../../shared/types';
+import { WSRoleMapping } from '../../workplace_search/types';
+
+import {
+ INVITATION_PENDING_LABEL,
+ ALL_LABEL,
+ FILTER_USERS_LABEL,
+ NO_USERS_LABEL,
+ ROLE_LABEL,
+ USERNAME_LABEL,
+ EMAIL_LABEL,
+ GROUPS_LABEL,
+ ENGINES_LABEL,
+} from './constants';
+
+import { UsersAndRolesRowActions } from './';
+
+interface AccessItem {
+ name: string;
+}
+
+interface SharedUser extends SingleUserRoleMapping {
+ accessItems: AccessItem[];
+ username: string;
+ email: string | null;
+ roleType: string;
+ id: string;
+}
+
+interface SharedRoleMapping extends ASRoleMapping, WSRoleMapping {
+ accessItems: AccessItem[];
+}
+
+interface Props {
+ accessItemKey: 'groups' | 'engines';
+ singleUserRoleMappings: Array>;
+ initializeSingleUserRoleMapping(roleId: string): string;
+ handleDeleteMapping(roleId: string): string;
+}
+
+const noItemsPlaceholder = — ;
+const invitationBadge = {INVITATION_PENDING_LABEL} ;
+
+export const UsersTable: React.FC = ({
+ accessItemKey,
+ singleUserRoleMappings,
+ initializeSingleUserRoleMapping,
+ handleDeleteMapping,
+}) => {
+ // 'accessItems' is needed because App Search has `engines` and Workplace Search has `groups`.
+ const users = ((singleUserRoleMappings as SharedUser[]).map((user) => ({
+ username: user.elasticsearchUser.username,
+ email: user.elasticsearchUser.email,
+ roleType: user.roleMapping.roleType,
+ id: user.roleMapping.id,
+ accessItems: (user.roleMapping as SharedRoleMapping)[accessItemKey],
+ invitation: user.invitation,
+ })) as unknown) as Array>;
+
+ const columns: Array> = [
+ {
+ field: 'username',
+ name: USERNAME_LABEL,
+ render: (_, { username }: SharedUser) => username,
+ },
+ {
+ field: 'email',
+ name: EMAIL_LABEL,
+ render: (_, { email, invitation }: SharedUser) => {
+ if (!email) return noItemsPlaceholder;
+ return (
+
+ {email} {invitation && invitationBadge}
+
+ );
+ },
+ },
+ {
+ field: 'roleType',
+ name: ROLE_LABEL,
+ render: (_, user: SharedUser) => user.roleType,
+ },
+ {
+ field: 'accessItems',
+ name: accessItemKey === 'groups' ? GROUPS_LABEL : ENGINES_LABEL,
+ render: (_, { accessItems }: SharedUser) => {
+ // Design calls for showing the first 2 items followed by a +x after those 2.
+ // ['foo', 'bar', 'baz'] would display as: "foo, bar + 1"
+ const numItems = accessItems.length;
+ if (numItems === 0) return {ALL_LABEL} ;
+ const additionalItems = numItems > 2 ? ` + ${numItems - 2}` : '';
+ const names = accessItems.map((item) => item.name);
+ return (
+ {names.slice(0, 2).join(', ') + additionalItems}
+ );
+ },
+ },
+ {
+ field: 'id',
+ name: '',
+ render: (_, { id, username }: SharedUser) => (
+ initializeSingleUserRoleMapping(id)}
+ onDeleteClick={() => handleDeleteMapping(id)}
+ />
+ ),
+ },
+ ];
+
+ const pagination = {
+ hidePerPageOptions: true,
+ pageSize: 10,
+ };
+
+ const search = {
+ box: {
+ incremental: true,
+ fullWidth: false,
+ placeholder: FILTER_USERS_LABEL,
+ 'data-test-subj': 'UsersTableSearchInput',
+ },
+ };
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
index 902417d02665e..ba9da900c0145 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
@@ -10,6 +10,7 @@ import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
+ EuiCallOut,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
@@ -83,8 +84,13 @@ export const SchemaAddFieldModal: React.FC = ({
{ADD_FIELD_MODAL_TITLE}
- {ADD_FIELD_MODAL_DESCRIPTION}
-
+ {ADD_FIELD_MODAL_DESCRIPTION}}
+ />
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
index 67208c63ddf4c..e6d2c67d1baf8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
@@ -40,3 +40,19 @@ export interface RoleMapping {
const productNames = [APP_SEARCH_PLUGIN.NAME, WORKPLACE_SEARCH_PLUGIN.NAME] as const;
export type ProductName = typeof productNames[number];
+
+export interface Invitation {
+ email: string;
+ code: string;
+}
+
+export interface ElasticsearchUser {
+ email: string | null;
+ username: string;
+}
+
+export interface SingleUserRoleMapping {
+ invitation: Invitation;
+ elasticsearchUser: ElasticsearchUser;
+ roleMapping: T;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
index 04b0880a7351c..f2601ff98db1d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
@@ -7,7 +7,7 @@
jest.mock('../../../shared/layout', () => ({
...jest.requireActual('../../../shared/layout'),
- generateNavLink: jest.fn(({ to }) => ({ href: to })),
+ generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })),
}));
jest.mock('../../views/content_sources/components/source_sub_nav', () => ({
useSourceSubNav: () => [],
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
index 99225bc36e892..ce2f8bf7ef7e4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
@@ -33,8 +33,11 @@ export const useWorkplaceSearchNav = () => {
{
id: 'sources',
name: NAV.SOURCES,
- ...generateNavLink({ to: SOURCES_PATH }),
- items: useSourceSubNav(),
+ ...generateNavLink({
+ to: SOURCES_PATH,
+ shouldShowActiveForSubroutes: true,
+ items: useSourceSubNav(),
+ }),
},
{
id: 'groups',
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
index 36496b83b3123..3f6863175e29b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
@@ -43,7 +43,6 @@ export const PrivateSourcesSidebar = () => {
return (
<>
- {/* @ts-expect-error: TODO, uncomment this once EUI 34.x lands in Kibana & `mobileBreakpoints` is a valid prop */}
{id && }
>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
index a4eb228eff92f..050aaf1dadf89 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
@@ -11,8 +11,8 @@ import { useValues } from 'kea';
import { EuiTable, EuiTableBody, EuiTablePagination } from '@elastic/eui';
import { Pager } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
+import { USERNAME_LABEL, EMAIL_LABEL } from '../../../../shared/constants';
import { TableHeader } from '../../../../shared/table_header';
import { AppLogic } from '../../../app_logic';
import { UserRow } from '../../../components/shared/user_row';
@@ -20,27 +20,15 @@ import { User } from '../../../types';
import { GroupLogic } from '../group_logic';
const USERS_PER_PAGE = 10;
-const USERNAME_TABLE_HEADER = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader',
- {
- defaultMessage: 'Username',
- }
-);
-const EMAIL_TABLE_HEADER = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader',
- {
- defaultMessage: 'Email',
- }
-);
export const GroupUsersTable: React.FC = () => {
const { isFederatedAuth } = useValues(AppLogic);
const {
group: { users },
} = useValues(GroupLogic);
- const headerItems = [USERNAME_TABLE_HEADER];
+ const headerItems = [USERNAME_LABEL];
if (!isFederatedAuth) {
- headerItems.push(EMAIL_TABLE_HEADER);
+ headerItems.push(EMAIL_LABEL);
}
const [firstItem, setFirstItem] = useState(0);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
index 92c8b7827b9b6..809b631c78391 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
@@ -7,14 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage',
- {
- defaultMessage:
- 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.',
- }
-);
-
export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.roleMappingDeletedMessage',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
index b153d01224193..01d32bec14ebd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
@@ -10,9 +10,14 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping';
+import {
+ RoleMappingsTable,
+ RoleMappingsHeading,
+ RolesEmptyPrompt,
+} from '../../../shared/role_mapping';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
import { WorkplaceSearchPageTemplate } from '../../components/layout';
+import { SECURITY_DOCS_URL } from '../../routes';
import { ROLE_MAPPINGS_TABLE_HEADER } from './constants';
@@ -20,9 +25,12 @@ import { RoleMapping } from './role_mapping';
import { RoleMappingsLogic } from './role_mappings_logic';
export const RoleMappings: React.FC = () => {
- const { initializeRoleMappings, initializeRoleMapping, handleDeleteMapping } = useActions(
- RoleMappingsLogic
- );
+ const {
+ enableRoleBasedAccess,
+ initializeRoleMappings,
+ initializeRoleMapping,
+ handleDeleteMapping,
+ } = useActions(RoleMappingsLogic);
const {
roleMappings,
@@ -35,10 +43,19 @@ export const RoleMappings: React.FC = () => {
initializeRoleMappings();
}, []);
+ const rolesEmptyState = (
+
+ );
+
const roleMappingsSection = (
initializeRoleMapping()}
/>
{
pageChrome={[ROLE_MAPPINGS_TITLE]}
pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }}
isLoading={dataLoading}
+ isEmptyState={roleMappings.length < 1}
+ emptyState={rolesEmptyState}
>
{roleMappingFlyoutOpen && }
{roleMappingsSection}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
index 4ee530870284e..a4bbddbd23b49 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
@@ -90,6 +90,13 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
});
+ it('setRoleMappings', () => {
+ RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [wsRoleMapping] });
+
+ expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]);
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
+ });
+
it('handleRoleChange', () => {
RoleMappingsLogic.actions.handleRoleChange('user');
@@ -234,6 +241,30 @@ describe('RoleMappingsLogic', () => {
});
describe('listeners', () => {
+ describe('enableRoleBasedAccess', () => {
+ it('calls API and sets values', async () => {
+ const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings');
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(true);
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/workplace_search/org/role_mappings/enable_role_based_access'
+ );
+ await nextTick();
+ expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps);
+ });
+
+ it('handles error', async () => {
+ http.post.mockReturnValue(Promise.reject('this is an error'));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ });
+ });
+
describe('initializeRoleMappings', () => {
it('calls API and sets values', async () => {
const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData');
@@ -351,18 +382,8 @@ describe('RoleMappingsLogic', () => {
});
describe('handleDeleteMapping', () => {
- let confirmSpy: any;
const roleMappingId = 'r1';
- beforeEach(() => {
- confirmSpy = jest.spyOn(window, 'confirm');
- confirmSpy.mockImplementation(jest.fn(() => true));
- });
-
- afterEach(() => {
- confirmSpy.mockRestore();
- });
-
it('calls API and refreshes list', async () => {
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
@@ -388,15 +409,6 @@ describe('RoleMappingsLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
});
-
- it('will do nothing if not confirmed', async () => {
- RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
- window.confirm = () => false;
- RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
-
- expect(http.delete).not.toHaveBeenCalled();
- await nextTick();
- });
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
index 361425b7a78a1..76b41b2f383eb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
@@ -20,7 +20,6 @@ import { AttributeName } from '../../../shared/types';
import { RoleGroup, WSRoleMapping, Role } from '../../types';
import {
- DELETE_ROLE_MAPPING_MESSAGE,
ROLE_MAPPING_DELETED_MESSAGE,
ROLE_MAPPING_CREATED_MESSAGE,
ROLE_MAPPING_UPDATED_MESSAGE,
@@ -57,10 +56,16 @@ interface RoleMappingsActions {
initializeRoleMappings(): void;
resetState(): void;
setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping };
+ setRoleMappings({
+ roleMappings,
+ }: {
+ roleMappings: WSRoleMapping[];
+ }): { roleMappings: WSRoleMapping[] };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
setRoleMappingErrors(errors: string[]): { errors: string[] };
+ enableRoleBasedAccess(): void;
}
interface RoleMappingsValues {
@@ -88,6 +93,7 @@ export const RoleMappingsLogic = kea data,
setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }),
+ setRoleMappings: ({ roleMappings }: { roleMappings: WSRoleMapping[] }) => ({ roleMappings }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string[]) => ({ value }),
handleRoleChange: (roleType: Role) => ({ roleType }),
@@ -98,6 +104,7 @@ export const RoleMappingsLogic = kea ({ value }),
handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }),
+ enableRoleBasedAccess: true,
resetState: true,
initializeRoleMappings: true,
initializeRoleMapping: (roleMappingId?: string) => ({ roleMappingId }),
@@ -111,13 +118,16 @@ export const RoleMappingsLogic = kea false,
+ setRoleMappings: () => false,
resetState: () => true,
+ enableRoleBasedAccess: () => true,
},
],
roleMappings: [
[],
{
setRoleMappingsData: (_, { roleMappings }) => roleMappings,
+ setRoleMappings: (_, { roleMappings }) => roleMappings,
resetState: () => [],
},
],
@@ -260,6 +270,17 @@ export const RoleMappingsLogic = kea ({
+ enableRoleBasedAccess: async () => {
+ const { http } = HttpLogic.values;
+ const route = '/api/workplace_search/org/role_mappings/enable_role_based_access';
+
+ try {
+ const response = await http.post(route);
+ actions.setRoleMappings(response);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
initializeRoleMappings: async () => {
const { http } = HttpLogic.values;
const route = '/api/workplace_search/org/role_mappings';
@@ -279,14 +300,12 @@ export const RoleMappingsLogic = kea {
diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
index 350c27fa43cd3..5580c3dac5996 100644
--- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
+++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
@@ -25,8 +25,7 @@ describe('App Search Telemetry Usage Collector', () => {
'ui_error.cannot_connect': 3,
'ui_error.not_found': 7,
'ui_clicked.create_first_engine_button': 40,
- 'ui_clicked.header_launch_button': 50,
- 'ui_clicked.engine_table_link': 60,
+ 'ui_clicked.engine_table_link': 50,
},
}),
incrementCounter: jest.fn(),
@@ -66,8 +65,7 @@ describe('App Search Telemetry Usage Collector', () => {
},
ui_clicked: {
create_first_engine_button: 40,
- header_launch_button: 50,
- engine_table_link: 60,
+ engine_table_link: 50,
},
});
});
@@ -93,7 +91,6 @@ describe('App Search Telemetry Usage Collector', () => {
},
ui_clicked: {
create_first_engine_button: 0,
- header_launch_button: 0,
engine_table_link: 0,
},
});
diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
index 36ba2976f929a..4dca6ed58e0c5 100644
--- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
+++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
@@ -23,7 +23,6 @@ interface Telemetry {
};
ui_clicked: {
create_first_engine_button: number;
- header_launch_button: number;
engine_table_link: number;
};
}
@@ -54,7 +53,6 @@ export const registerTelemetryUsageCollector = (
},
ui_clicked: {
create_first_engine_button: { type: 'long' },
- header_launch_button: { type: 'long' },
engine_table_link: { type: 'long' },
},
},
@@ -85,7 +83,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log
},
ui_clicked: {
create_first_engine_button: 0,
- header_launch_button: 0,
engine_table_link: 0,
},
};
@@ -110,7 +107,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log
'ui_clicked.create_first_engine_button',
0
),
- header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0),
engine_table_link: get(savedObjectAttributes, 'ui_clicked.engine_table_link', 0),
},
} as Telemetry;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
index 718597c12e9c5..7d9f08627516b 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
@@ -7,7 +7,11 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings';
+import {
+ registerEnableRoleMappingsRoute,
+ registerRoleMappingsRoute,
+ registerRoleMappingRoute,
+} from './role_mappings';
const roleMappingBaseSchema = {
rules: { username: 'user' },
@@ -18,6 +22,29 @@ const roleMappingBaseSchema = {
};
describe('role mappings routes', () => {
+ describe('POST /api/app_search/role_mappings/enable_role_based_access', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/role_mappings/enable_role_based_access',
+ });
+
+ registerEnableRoleMappingsRoute({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request handler', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/role_mappings/enable_role_based_access',
+ });
+ });
+ });
+
describe('GET /api/app_search/role_mappings', () => {
let mockRouter: MockRouter;
@@ -36,7 +63,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings',
+ path: '/as/role_mappings',
});
});
});
@@ -59,7 +86,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings',
+ path: '/as/role_mappings',
});
});
@@ -94,7 +121,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
});
});
@@ -129,7 +156,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
index 75724a3344d6d..da620be2ea950 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
@@ -17,6 +17,21 @@ const roleMappingBaseSchema = {
authProvider: schema.arrayOf(schema.string()),
};
+export function registerEnableRoleMappingsRoute({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.post(
+ {
+ path: '/api/app_search/role_mappings/enable_role_based_access',
+ validate: false,
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/as/role_mappings/enable_role_based_access',
+ })
+ );
+}
+
export function registerRoleMappingsRoute({
router,
enterpriseSearchRequestHandler,
@@ -27,7 +42,7 @@ export function registerRoleMappingsRoute({
validate: false,
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings',
+ path: '/as/role_mappings',
})
);
@@ -39,7 +54,7 @@ export function registerRoleMappingsRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings',
+ path: '/as/role_mappings',
})
);
}
@@ -59,7 +74,7 @@ export function registerRoleMappingRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
})
);
@@ -73,12 +88,13 @@ export function registerRoleMappingRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
})
);
}
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
+ registerEnableRoleMappingsRoute(dependencies);
registerRoleMappingsRoute(dependencies);
registerRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
index a945866da5ef2..aa0e9983166c0 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
@@ -7,9 +7,36 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings';
+import {
+ registerOrgEnableRoleMappingsRoute,
+ registerOrgRoleMappingsRoute,
+ registerOrgRoleMappingRoute,
+} from './role_mappings';
describe('role mappings routes', () => {
+ describe('POST /api/workplace_search/org/role_mappings/enable_role_based_access', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/workplace_search/org/role_mappings/enable_role_based_access',
+ });
+
+ registerOrgEnableRoleMappingsRoute({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request handler', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/ws/org/role_mappings/enable_role_based_access',
+ });
+ });
+ });
+
describe('GET /api/workplace_search/org/role_mappings', () => {
let mockRouter: MockRouter;
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
index a0fcec63cbb27..cea7bcb311ce8 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
@@ -17,6 +17,21 @@ const roleMappingBaseSchema = {
authProvider: schema.arrayOf(schema.string()),
};
+export function registerOrgEnableRoleMappingsRoute({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.post(
+ {
+ path: '/api/workplace_search/org/role_mappings/enable_role_based_access',
+ validate: false,
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/ws/org/role_mappings/enable_role_based_access',
+ })
+ );
+}
+
export function registerOrgRoleMappingsRoute({
router,
enterpriseSearchRequestHandler,
@@ -79,6 +94,7 @@ export function registerOrgRoleMappingRoute({
}
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
+ registerOrgEnableRoleMappingsRoute(dependencies);
registerOrgRoleMappingsRoute(dependencies);
registerOrgRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md
index 032f77543acb9..ffbd20dd6f2be 100644
--- a/x-pack/plugins/event_log/README.md
+++ b/x-pack/plugins/event_log/README.md
@@ -131,7 +131,7 @@ Below is a document in the expected structure, with descriptions of the fields:
instance_id: "alert instance id, for relevant documents",
action_group_id: "alert action group, for relevant documents",
action_subgroup: "alert action subgroup, for relevant documents",
- status: "overall alert status, after alert execution",
+ status: "overall alert status, after rule execution",
},
saved_objects: [
{
@@ -160,21 +160,26 @@ plugins:
- `action: execute-via-http` - generated when an action is executed via HTTP request
- `provider: alerting`
- - `action: execute` - generated when an alert executor runs
- - `action: execute-action` - generated when an alert schedules an action to run
- - `action: new-instance` - generated when an alert has a new instance id that is active
- - `action: recovered-instance` - generated when an alert has a previously active instance id that is no longer active
- - `action: active-instance` - generated when an alert determines an instance id is active
+ - `action: execute` - generated when a rule executor runs
+ - `action: execute-action` - generated when a rule schedules an action to run
+ - `action: new-instance` - generated when a rule has a new instance id that is active
+ - `action: recovered-instance` - generated when a rule has a previously active instance id that is no longer active
+ - `action: active-instance` - generated when a rule determines an instance id is active
For the `saved_objects` array elements, these are references to saved objects
-associated with the event. For the `alerting` provider, those are alert saved
-ojects and for the `actions` provider those are action saved objects. The
-`alerts:execute-action` event includes both the alert and action saved object
-references. For that event, only the alert reference has the optional `rel`
+associated with the event. For the `alerting` provider, those are rule saved
+ojects and for the `actions` provider those are connector saved objects. The
+`alerts:execute-action` event includes both the rule and connector saved object
+references. For that event, only the rule reference has the optional `rel`
property with a `primary` value. This property is used when searching the
event log to indicate which saved objects should be directly searchable via
-saved object references. For the `alerts:execute-action` event, searching
-only via the alert saved object reference will return the event.
+saved object references. For the `alerts:execute-action` event, only searching
+via the rule saved object reference will return the event; searching via the
+connector save object reference will **NOT** return the event. The
+`actions:execute` event also includes both the rule and connector saved object
+references, and both of them have the `rel` property with a `primary` value,
+allowing those events to be returned in searches of either the rule or
+connector.
## Event Log index - associated resources
diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts
index e9dd968d3f048..81ea2a630d3db 100644
--- a/x-pack/plugins/fleet/common/constants/epm.ts
+++ b/x-pack/plugins/fleet/common/constants/epm.ts
@@ -48,6 +48,9 @@ export const dataTypes = {
Metrics: 'metrics',
} as const;
+// currently identical but may be a subset or otherwise different some day
+export const monitoringTypes = Object.values(dataTypes);
+
export const installationStatuses = {
Installed: 'installed',
NotInstalled: 'not_installed',
diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts
index 937c08b7e8cb5..2ec67393df76b 100644
--- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts
+++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts
@@ -12,6 +12,7 @@ import {
FLEET_SYSTEM_PACKAGE,
FLEET_SERVER_PACKAGE,
autoUpdatePackages,
+ monitoringTypes,
} from './epm';
export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE =
@@ -40,7 +41,7 @@ export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
],
is_default: true,
is_managed: false,
- monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
+ monitoring_enabled: monitoringTypes,
};
export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
@@ -58,7 +59,7 @@ export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefa
is_default: false,
is_default_fleet_server: true,
is_managed: false,
- monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
+ monitoring_enabled: monitoringTypes,
};
export const DEFAULT_PACKAGES = defaultPackages.map((name) => ({
diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts
index 037c0ee506a05..0b892bacf53a7 100644
--- a/x-pack/plugins/fleet/common/constants/routes.ts
+++ b/x-pack/plugins/fleet/common/constants/routes.ts
@@ -117,5 +117,5 @@ export const INSTALL_SCRIPT_API_ROUTES = `${API_ROOT}/install/{osType}`;
// Policy preconfig API routes
export const PRECONFIGURATION_API_ROUTES = {
- PUT_PRECONFIG: `${API_ROOT}/setup/preconfiguration`,
+ UPDATE_PATTERN: `${API_ROOT}/setup/preconfiguration`,
};
diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.test.ts b/x-pack/plugins/fleet/common/services/hosts_utils.test.ts
similarity index 100%
rename from x-pack/plugins/fleet/server/services/hosts_utils.test.ts
rename to x-pack/plugins/fleet/common/services/hosts_utils.test.ts
diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.ts b/x-pack/plugins/fleet/common/services/hosts_utils.ts
similarity index 100%
rename from x-pack/plugins/fleet/server/services/hosts_utils.ts
rename to x-pack/plugins/fleet/common/services/hosts_utils.ts
diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts
index 86361ae163399..a6f4cd319b970 100644
--- a/x-pack/plugins/fleet/common/services/index.ts
+++ b/x-pack/plugins/fleet/common/services/index.ts
@@ -30,3 +30,5 @@ export {
validationHasErrors,
countValidationErrors,
} from './validate_package_policy';
+
+export { normalizeHostsForAgents } from './hosts_utils';
diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts
index 95f91165aaf94..59691bf32d099 100644
--- a/x-pack/plugins/fleet/common/types/index.ts
+++ b/x-pack/plugins/fleet/common/types/index.ts
@@ -25,6 +25,7 @@ export interface FleetConfigType {
};
agentPolicies?: PreconfiguredAgentPolicy[];
packages?: PreconfiguredPackage[];
+ agentIdVerificationEnabled?: boolean;
}
// Calling Object.entries(PackagesGroupedByStatus) gave `status: string`
diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
index a9393abcc57ef..f64467ca674fb 100644
--- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
@@ -6,7 +6,7 @@
*/
import type { agentPolicyStatuses } from '../../constants';
-import type { DataType, ValueOf } from '../../types';
+import type { MonitoringType, ValueOf } from '../../types';
import type { PackagePolicy, PackagePolicyPackage } from './package_policy';
import type { Output } from './output';
@@ -20,7 +20,8 @@ export interface NewAgentPolicy {
is_default?: boolean;
is_default_fleet_server?: boolean; // Optional when creating a policy
is_managed?: boolean; // Optional when creating a policy
- monitoring_enabled?: Array>;
+ monitoring_enabled?: MonitoringType;
+ unenroll_timeout?: number;
is_preconfigured?: boolean;
}
@@ -138,4 +139,8 @@ export interface FleetServerPolicy {
* True when this policy is the default policy to start Fleet Server
*/
default_fleet_server: boolean;
+ /**
+ * Auto unenroll any Elastic Agents which have not checked in for this many seconds
+ */
+ unenroll_timeout?: number;
}
diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index aece658083196..36554b8409364 100644
--- a/x-pack/plugins/fleet/common/types/models/epm.ts
+++ b/x-pack/plugins/fleet/common/types/models/epm.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { estypes } from '@elastic/elasticsearch';
// Follow pattern from https://github.com/elastic/kibana/pull/52447
// TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed
import type { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public';
@@ -13,6 +14,7 @@ import type {
ASSETS_SAVED_OBJECT_TYPE,
agentAssetTypes,
dataTypes,
+ monitoringTypes,
installationStatuses,
} from '../../constants';
import type { ValueOf } from '../../types';
@@ -91,7 +93,7 @@ export enum ElasticsearchAssetType {
}
export type DataType = typeof dataTypes;
-
+export type MonitoringType = typeof monitoringTypes;
export type InstallablePackage = RegistryPackage | ArchivePackage;
export type ArchivePackage = PackageSpecManifest &
@@ -299,8 +301,8 @@ export interface RegistryDataStream {
}
export interface RegistryElasticsearch {
- 'index_template.settings'?: object;
- 'index_template.mappings'?: object;
+ 'index_template.settings'?: estypes.IndicesIndexSettings;
+ 'index_template.mappings'?: estypes.MappingTypeMapping;
}
export interface RegistryDataStreamPermissions {
@@ -425,7 +427,7 @@ export interface IndexTemplate {
_meta: object;
}
-export interface TemplateRef {
+export interface IndexTemplateEntry {
templateName: string;
indexTemplate: IndexTemplate;
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
index 25a0993242822..633f8a2c57409 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
@@ -21,6 +21,7 @@ import {
EuiCheckboxGroup,
EuiButton,
EuiLink,
+ EuiFieldNumber,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -158,6 +159,10 @@ export const AgentPolicyForm: React.FunctionComponent = ({
);
});
+ const unenrollmentTimeoutText = i18n.translate(
+ 'xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel',
+ { defaultMessage: 'Unenrollment timeout' }
+ );
const advancedOptionsContent = (
<>
@@ -297,6 +302,27 @@ export const AgentPolicyForm: React.FunctionComponent = ({
}}
/>
+ {unenrollmentTimeoutText}}
+ description={
+
+ }
+ >
+
+ updateAgentPolicy({ unenroll_timeout: Number(e.target.value) })}
+ isInvalid={Boolean(touchedFields.unenroll_timeout && validation.unenroll_timeout)}
+ onBlur={() => setTouchedFields({ ...touchedFields, unenroll_timeout: true })}
+ placeholder={unenrollmentTimeoutText}
+ />
+
+
{isEditing &&
'id' in agentPolicy &&
!agentPolicy.is_managed &&
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
index 1ea1a7de53b95..0c6451e3f34a2 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
@@ -65,12 +65,13 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
setIsLoading(true);
try {
// eslint-disable-next-line @typescript-eslint/naming-convention
- const { name, description, namespace, monitoring_enabled } = agentPolicy;
+ const { name, description, namespace, monitoring_enabled, unenroll_timeout } = agentPolicy;
const { data, error } = await sendUpdateAgentPolicy(agentPolicy.id, {
name,
description,
namespace,
monitoring_enabled,
+ unenroll_timeout,
});
if (data) {
notifications.toasts.addSuccess(
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
index 33dbbb25c5d42..5992888564e7f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
@@ -5,7 +5,9 @@
* 2.0.
*/
+import type { ReactNode } from 'react';
import React, { useState } from 'react';
+import type { StyledComponent } from 'styled-components';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -29,7 +31,13 @@ import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useCapabilities, useStartServices, sendCreateAgentPolicy } from '../../../../hooks';
import { AgentPolicyForm, agentPolicyFormValidation } from '../../components';
-const FlyoutWithHigherZIndex = styled(EuiFlyout)`
+// TODO: EUI team follow up on complex types and styled-components `styled`
+// https://github.com/elastic/eui/issues/4855
+const FlyoutWithHigherZIndex: StyledComponent<
+ typeof EuiFlyout,
+ {},
+ { children?: ReactNode }
+> = styled(EuiFlyout)`
z-index: ${(props) => props.theme.eui.euiZLevel5};
`;
@@ -39,6 +47,7 @@ interface Props extends EuiFlyoutProps {
export const CreateAgentPolicyFlyout: React.FunctionComponent = ({
onClose,
+ as,
...restOfProps
}) => {
const { notifications } = useStartServices();
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
index 995423ea91f96..9e8d200344b01 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
@@ -233,7 +233,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => {
,
diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
index d748e655bd506..9bc1bc977b786 100644
--- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
@@ -38,7 +38,7 @@ import {
useGetOutputs,
sendPutOutput,
} from '../../hooks';
-import { isDiffPathProtocol } from '../../../common';
+import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common';
import { SettingsConfirmModal } from './confirm_modal';
import type { SettingsConfirmModalProps } from './confirm_modal';
@@ -53,8 +53,20 @@ interface Props {
onClose: () => void;
}
-function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) {
- return arrayA.length === arrayB.length && arrayA.every((val, index) => val === arrayB[index]);
+function normalizeHosts(hostsInput: string[]) {
+ return hostsInput.map((host) => {
+ try {
+ return normalizeHostsForAgents(host);
+ } catch (err) {
+ return host;
+ }
+ });
+}
+
+function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) {
+ const hostsA = normalizeHosts(arrayA);
+ const hostsB = normalizeHosts(arrayB);
+ return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]);
}
function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
@@ -234,8 +246,11 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
return false;
}
return (
- !isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value) ||
- !isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value) ||
+ !isSameArrayValueWithNormalizedHosts(
+ settings.fleet_server_hosts,
+ inputs.fleetServerHosts.value
+ ) ||
+ !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) ||
(output.config_yaml || '') !== inputs.additionalYamlConfig.value
);
}, [settings, inputs, output]);
@@ -246,32 +261,37 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
}
const tmpChanges: SettingsConfirmModalProps['changes'] = [];
- if (!isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value)) {
+ if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) {
tmpChanges.push(
{
type: 'elasticsearch',
direction: 'removed',
- urls: output.hosts || [],
+ urls: normalizeHosts(output.hosts || []),
},
{
type: 'elasticsearch',
direction: 'added',
- urls: inputs.elasticsearchUrl.value,
+ urls: normalizeHosts(inputs.elasticsearchUrl.value),
}
);
}
- if (!isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value)) {
+ if (
+ !isSameArrayValueWithNormalizedHosts(
+ settings.fleet_server_hosts,
+ inputs.fleetServerHosts.value
+ )
+ ) {
tmpChanges.push(
{
type: 'fleet_server',
direction: 'removed',
- urls: settings.fleet_server_hosts,
+ urls: normalizeHosts(settings.fleet_server_hosts || []),
},
{
type: 'fleet_server',
direction: 'added',
- urls: inputs.fleetServerHosts.value,
+ urls: normalizeHosts(inputs.fleetServerHosts.value),
}
);
}
@@ -300,7 +320,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
helpText={
= ({ onClose }) => {
defaultMessage: 'Elasticsearch hosts',
})}
helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', {
- defaultMessage: 'Specify the Elasticsearch URLs where agents send data.',
+ defaultMessage:
+ 'Specify the Elasticsearch URLs where agents send data. Elasticsearch uses port 9200 by default.',
})}
/>
diff --git a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
index 097b6aa98c067..5dad8ad504979 100644
--- a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
+++ b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
@@ -12,6 +12,7 @@ export const createConfigurationMock = (): FleetConfigType => {
enabled: true,
registryUrl: '',
registryProxyUrl: '',
+ agentIdVerificationEnabled: true,
agents: {
enabled: true,
elasticsearch: {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
similarity index 82%
rename from x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
rename to x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
index f929a4f139981..8e9dac11db799 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
+++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
@@ -5,9 +5,37 @@
* 2.0.
*/
-export const FINAL_PIPELINE_ID = '.fleet_final_pipeline';
+export const FLEET_FINAL_PIPELINE_ID = '.fleet_final_pipeline-1';
-export const FINAL_PIPELINE = `---
+export const FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME = '.fleet_component_template-1';
+
+export const FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT = {
+ _meta: {},
+ template: {
+ settings: {
+ index: {
+ final_pipeline: FLEET_FINAL_PIPELINE_ID,
+ },
+ },
+ mappings: {
+ properties: {
+ event: {
+ properties: {
+ ingested: {
+ type: 'date',
+ },
+ agent_id_status: {
+ ignore_above: 1024,
+ type: 'keyword',
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+export const FLEET_FINAL_PIPELINE_CONTENT = `---
description: >
Final pipeline for processing all incoming Fleet Agent documents.
processors:
diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts
index 16a92a2ffa1aa..3aca5e8800dc5 100644
--- a/x-pack/plugins/fleet/server/constants/index.ts
+++ b/x-pack/plugins/fleet/server/constants/index.ts
@@ -57,3 +57,10 @@ export {
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
PRECONFIGURATION_LATEST_KEYWORD,
} from '../../common';
+
+export {
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+ FLEET_FINAL_PIPELINE_ID,
+ FLEET_FINAL_PIPELINE_CONTENT,
+} from './fleet_es_assets';
diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts
index 0a886ffedbd6c..ab1cd9002d04a 100644
--- a/x-pack/plugins/fleet/server/index.ts
+++ b/x-pack/plugins/fleet/server/index.ts
@@ -77,6 +77,7 @@ export const config: PluginConfigDescriptor = {
}),
packages: PreconfiguredPackagesSchema,
agentPolicies: PreconfiguredAgentPoliciesSchema,
+ agentIdVerificationEnabled: schema.boolean({ defaultValue: true }),
}),
};
diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts
index a94f274b202ad..43a5a14b425b5 100644
--- a/x-pack/plugins/fleet/server/mocks/index.ts
+++ b/x-pack/plugins/fleet/server/mocks/index.ts
@@ -4,6 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { of } from 'rxjs';
+
import {
elasticsearchServiceMock,
loggingSystemMock,
@@ -22,6 +24,14 @@ import type { FleetAppContext } from '../plugin';
export * from '../services/artifacts/mocks';
export const createAppContextStartContractMock = (): FleetAppContext => {
+ const config = {
+ agents: { enabled: true, elasticsearch: {} },
+ enabled: true,
+ agentIdVerificationEnabled: true,
+ };
+
+ const config$ = of(config);
+
return {
elasticsearch: elasticsearchServiceMock.createStart(),
data: dataPluginMock.createStartContract(),
@@ -33,7 +43,9 @@ export const createAppContextStartContractMock = (): FleetAppContext => {
configInitialValue: {
agents: { enabled: true, elasticsearch: {} },
enabled: true,
+ agentIdVerificationEnabled: true,
},
+ config$,
kibanaVersion: '8.0.0',
kibanaBranch: 'master',
};
diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
index 77fe74fda54d9..d6c483ffe30d9 100644
--- a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
+++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
@@ -15,7 +15,7 @@ import { PutPreconfigurationSchema } from '../../types';
import { defaultIngestErrorHandler } from '../../errors';
import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services';
-export const putPreconfigurationHandler: RequestHandler<
+export const updatePreconfigurationHandler: RequestHandler<
undefined,
undefined,
TypeOf
@@ -43,10 +43,10 @@ export const putPreconfigurationHandler: RequestHandler<
export const registerRoutes = (router: IRouter) => {
router.put(
{
- path: PRECONFIGURATION_API_ROUTES.PUT_PRECONFIG,
+ path: PRECONFIGURATION_API_ROUTES.UPDATE_PATTERN,
validate: PutPreconfigurationSchema,
options: { tags: [`access:${PLUGIN_ID}-all`] },
},
- putPreconfigurationHandler
+ updatePreconfigurationHandler
);
};
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index bd7bb98eb7c07..fe8771115a217 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -149,6 +149,7 @@ const getSavedObjectTypes = (
is_managed: { type: 'boolean' },
status: { type: 'keyword' },
package_policies: { type: 'keyword' },
+ unenroll_timeout: { type: 'integer' },
updated_at: { type: 'date' },
updated_by: { type: 'keyword' },
revision: { type: 'integer' },
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 2a6036d99281e..465075cca7a0b 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -642,6 +642,7 @@ class AgentPolicyService {
data: (fullPolicy as unknown) as FleetServerPolicy['data'],
policy_id: fullPolicy.id,
default_fleet_server: policy.is_default_fleet_server === true,
+ unenroll_timeout: policy.unenroll_timeout,
};
await esClient.create({
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
index 1d212f188120f..a6aa87c5ed0f5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
@@ -14,9 +14,9 @@ import { getAsset, getPathParts } from '../../archive';
import type { ArchiveEntry } from '../../archive';
import { saveInstalledEsRefs } from '../../packages/install';
import { getInstallationObject } from '../../packages';
+import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID } from '../../../../constants';
import { deletePipelineRefs } from './remove';
-import { FINAL_PIPELINE, FINAL_PIPELINE_ID } from './final_pipeline';
interface RewriteSubstitution {
source: string;
@@ -190,22 +190,24 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc
const esClientRequestOptions: TransportRequestOptions = {
ignore: [404],
};
- const res = await esClient.ingest.getPipeline({ id: FINAL_PIPELINE_ID }, esClientRequestOptions);
+ const res = await esClient.ingest.getPipeline(
+ { id: FLEET_FINAL_PIPELINE_ID },
+ esClientRequestOptions
+ );
if (res.statusCode === 404) {
- await esClient.ingest.putPipeline(
- // @ts-ignore pipeline is define in yaml
- { id: FINAL_PIPELINE_ID, body: FINAL_PIPELINE },
- {
- headers: {
- // pipeline is YAML
- 'Content-Type': 'application/yaml',
- // but we want JSON responses (to extract error messages, status code, or other metadata)
- Accept: 'application/json',
- },
- }
- );
+ await installPipeline({
+ esClient,
+ pipeline: {
+ nameForInstallation: FLEET_FINAL_PIPELINE_ID,
+ contentForInstallation: FLEET_FINAL_PIPELINE_CONTENT,
+ extension: 'yml',
+ },
+ });
+ return { isCreated: true };
}
+
+ return { isCreated: false };
}
const isDirectory = ({ path }: ArchiveEntry) => path.endsWith('/');
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index acf8ae742bf8f..6a4476316bfa5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -25,8 +25,7 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = `
"default_field": [
"long.nested.foo"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -99,7 +98,9 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "nginx"
@@ -140,8 +141,7 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
"coredns.response.code",
"coredns.response.flags"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -214,7 +214,9 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "coredns"
@@ -283,8 +285,7 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = `
"system.users.scope",
"system.users.remote_host"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -1741,7 +1742,9 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "system"
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
index d202dab54f5bd..e8dac60ddba1a 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
@@ -11,7 +11,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s
import { ElasticsearchAssetType } from '../../../../types';
import type {
RegistryDataStream,
- TemplateRef,
+ IndexTemplateEntry,
RegistryElasticsearch,
InstallablePackage,
} from '../../../../types';
@@ -19,7 +19,11 @@ import { loadFieldsFromYaml, processFields } from '../../fields/field';
import type { Field } from '../../fields/field';
import { getPipelineNameForInstallation } from '../ingest_pipeline/install';
import { getAsset, getPathParts } from '../../archive';
-import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install';
+import { removeAssetTypesFromInstalledEs, saveInstalledEsRefs } from '../../packages/install';
+import {
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+} from '../../../../constants';
import {
generateMappings,
@@ -34,7 +38,7 @@ export const installTemplates = async (
esClient: ElasticsearchClient,
paths: string[],
savedObjectsClient: SavedObjectsClientContract
-): Promise => {
+): Promise => {
// install any pre-built index template assets,
// atm, this is only the base package's global index templates
// Install component templates first, as they are used by the index templates
@@ -42,44 +46,36 @@ export const installTemplates = async (
await installPreBuiltTemplates(paths, esClient);
// remove package installation's references to index templates
- await removeAssetsFromInstalledEsByType(
- savedObjectsClient,
- installablePackage.name,
- ElasticsearchAssetType.indexTemplate
- );
+ await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [
+ ElasticsearchAssetType.indexTemplate,
+ ElasticsearchAssetType.componentTemplate,
+ ]);
// build templates per data stream from yml files
const dataStreams = installablePackage.data_streams;
if (!dataStreams) return [];
+
+ const installedTemplatesNested = await Promise.all(
+ dataStreams.map((dataStream) =>
+ installTemplateForDataStream({
+ pkg: installablePackage,
+ esClient,
+ dataStream,
+ })
+ )
+ );
+ const installedTemplates = installedTemplatesNested.flat();
+
// get template refs to save
- const installedTemplateRefs = dataStreams.map((dataStream) => ({
- id: generateTemplateName(dataStream),
- type: ElasticsearchAssetType.indexTemplate,
- }));
+ const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates);
// add package installation's references to index templates
- await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, installedTemplateRefs);
-
- if (dataStreams) {
- const installTemplatePromises = dataStreams.reduce>>(
- (acc, dataStream) => {
- acc.push(
- installTemplateForDataStream({
- pkg: installablePackage,
- esClient,
- dataStream,
- })
- );
- return acc;
- },
- []
- );
-
- const res = await Promise.all(installTemplatePromises);
- const installedTemplates = res.flat();
+ await saveInstalledEsRefs(
+ savedObjectsClient,
+ installablePackage.name,
+ installedIndexTemplateRefs
+ );
- return installedTemplates;
- }
- return [];
+ return installedTemplates;
};
const installPreBuiltTemplates = async (paths: string[], esClient: ElasticsearchClient) => {
@@ -160,7 +156,7 @@ export async function installTemplateForDataStream({
pkg: InstallablePackage;
esClient: ElasticsearchClient;
dataStream: RegistryDataStream;
-}): Promise {
+}): Promise {
const fields = await loadFieldsFromYaml(pkg, dataStream.path);
return installTemplate({
esClient,
@@ -171,84 +167,140 @@ export async function installTemplateForDataStream({
});
}
+interface TemplateMapEntry {
+ _meta: { package?: { name: string } };
+ template:
+ | {
+ mappings: NonNullable;
+ }
+ | {
+ settings: NonNullable | object;
+ };
+}
+type TemplateMap = Record;
function putComponentTemplate(
- body: object | undefined,
- name: string,
- esClient: ElasticsearchClient
-): { clusterPromise: Promise; name: string } | undefined {
- if (body) {
- const esClientParams = {
- name,
- body,
- };
-
- return {
- // @ts-expect-error body expected to be ClusterPutComponentTemplateRequest
- clusterPromise: esClient.cluster.putComponentTemplate(esClientParams, { ignore: [404] }),
- name,
- };
+ esClient: ElasticsearchClient,
+ params: {
+ body: TemplateMapEntry;
+ name: string;
+ create?: boolean;
}
+): { clusterPromise: Promise; name: string } {
+ const { name, body, create = false } = params;
+ return {
+ clusterPromise: esClient.cluster.putComponentTemplate(
+ // @ts-expect-error body is missing required key `settings`. TemplateMapEntry has settings *or* mappings
+ { name, body, create },
+ { ignore: [404] }
+ ),
+ name,
+ };
}
-function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | undefined) {
- let mappingsTemplate;
- let settingsTemplate;
+const mappingsSuffix = '@mappings';
+const settingsSuffix = '@settings';
+const userSettingsSuffix = '@custom';
+type TemplateBaseName = string;
+type UserSettingsTemplateName = `${TemplateBaseName}${typeof userSettingsSuffix}`;
+
+const isUserSettingsTemplate = (name: string): name is UserSettingsTemplateName =>
+ name.endsWith(userSettingsSuffix);
+
+function buildComponentTemplates(params: {
+ templateName: string;
+ registryElasticsearch: RegistryElasticsearch | undefined;
+ packageName: string;
+}) {
+ const { templateName, registryElasticsearch, packageName } = params;
+ const mappingsTemplateName = `${templateName}${mappingsSuffix}`;
+ const settingsTemplateName = `${templateName}${settingsSuffix}`;
+ const userSettingsTemplateName = `${templateName}${userSettingsSuffix}`;
+
+ const templatesMap: TemplateMap = {};
+ const _meta = { package: { name: packageName } };
if (registryElasticsearch && registryElasticsearch['index_template.mappings']) {
- mappingsTemplate = {
+ templatesMap[mappingsTemplateName] = {
template: {
- mappings: {
- ...registryElasticsearch['index_template.mappings'],
- },
+ mappings: registryElasticsearch['index_template.mappings'],
},
+ _meta,
};
}
if (registryElasticsearch && registryElasticsearch['index_template.settings']) {
- settingsTemplate = {
+ templatesMap[settingsTemplateName] = {
template: {
settings: registryElasticsearch['index_template.settings'],
},
+ _meta,
};
}
- return { settingsTemplate, mappingsTemplate };
-}
-async function installDataStreamComponentTemplates(
- templateName: string,
- registryElasticsearch: RegistryElasticsearch | undefined,
- esClient: ElasticsearchClient
-) {
- const templates: string[] = [];
- const componentPromises: Array> = [];
+ // return empty/stub template
+ templatesMap[userSettingsTemplateName] = {
+ template: {
+ settings: {},
+ },
+ _meta,
+ };
- const compTemplates = buildComponentTemplates(registryElasticsearch);
+ return templatesMap;
+}
- const mappings = putComponentTemplate(
- compTemplates.mappingsTemplate,
- `${templateName}-mappings`,
- esClient
- );
+async function installDataStreamComponentTemplates(params: {
+ templateName: string;
+ registryElasticsearch: RegistryElasticsearch | undefined;
+ esClient: ElasticsearchClient;
+ packageName: string;
+}) {
+ const { templateName, registryElasticsearch, esClient, packageName } = params;
+ const templates = buildComponentTemplates({ templateName, registryElasticsearch, packageName });
+ const templateNames = Object.keys(templates);
+ const templateEntries = Object.entries(templates);
- const settings = putComponentTemplate(
- compTemplates.settingsTemplate,
- `${templateName}-settings`,
- esClient
+ // TODO: Check return values for errors
+ await Promise.all(
+ templateEntries.map(async ([name, body]) => {
+ if (isUserSettingsTemplate(name)) {
+ // look for existing user_settings template
+ const result = await esClient.cluster.getComponentTemplate({ name }, { ignore: [404] });
+ const hasUserSettingsTemplate = result.body.component_templates?.length === 1;
+ if (!hasUserSettingsTemplate) {
+ // only add if one isn't already present
+ const { clusterPromise } = putComponentTemplate(esClient, { body, name, create: true });
+ return clusterPromise;
+ }
+ } else {
+ const { clusterPromise } = putComponentTemplate(esClient, { body, name });
+ return clusterPromise;
+ }
+ })
);
- if (mappings) {
- templates.push(mappings.name);
- componentPromises.push(mappings.clusterPromise);
- }
+ return templateNames;
+}
- if (settings) {
- templates.push(settings.name);
- componentPromises.push(settings.clusterPromise);
+export async function ensureDefaultComponentTemplate(esClient: ElasticsearchClient) {
+ const { body: getTemplateRes } = await esClient.cluster.getComponentTemplate(
+ {
+ name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ },
+ {
+ ignore: [404],
+ }
+ );
+
+ const existingTemplate = getTemplateRes?.component_templates?.[0];
+ if (!existingTemplate) {
+ await putComponentTemplate(esClient, {
+ name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+ create: true,
+ });
}
- // TODO: Check return values for errors
- await Promise.all(componentPromises);
- return templates;
+ return { isCreated: !existingTemplate };
}
export async function installTemplate({
@@ -263,7 +315,7 @@ export async function installTemplate({
dataStream: RegistryDataStream;
packageVersion: string;
packageName: string;
-}): Promise {
+}): Promise {
const validFields = processFields(fields);
const mappings = generateMappings(validFields);
const templateName = generateTemplateName(dataStream);
@@ -310,11 +362,12 @@ export async function installTemplate({
await esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] });
}
- const composedOfTemplates = await installDataStreamComponentTemplates(
+ const composedOfTemplates = await installDataStreamComponentTemplates({
templateName,
- dataStream.elasticsearch,
- esClient
- );
+ registryElasticsearch: dataStream.elasticsearch,
+ esClient,
+ packageName,
+ });
const template = getTemplate({
type: dataStream.type,
@@ -342,3 +395,22 @@ export async function installTemplate({
indexTemplate: template,
};
}
+
+export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) {
+ return installedTemplates.flatMap((installedTemplate) => {
+ const indexTemplates = [
+ {
+ id: installedTemplate.templateName,
+ type: ElasticsearchAssetType.indexTemplate,
+ },
+ ];
+ const componentTemplates = installedTemplate.indexTemplate.composed_of
+ // Filter global component template shared between integrations
+ .filter((componentTemplateId) => componentTemplateId !== FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME)
+ .map((componentTemplateId) => ({
+ id: componentTemplateId,
+ type: ElasticsearchAssetType.componentTemplate,
+ }));
+ return indexTemplates.concat(componentTemplates);
+ });
+}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
index ae7bff618dba2..d1f806f67ca5c 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
@@ -24,6 +24,8 @@ import {
generateTemplateIndexPattern,
} from './template';
+const FLEET_COMPONENT_TEMPLATE = '.fleet_component_template-1';
+
// Add our own serialiser to just do JSON.stringify
expect.addSnapshotSerializer({
print(val) {
@@ -67,7 +69,7 @@ describe('EPM template', () => {
composedOfTemplates,
templatePriority: 200,
});
- expect(template.composed_of).toStrictEqual(composedOfTemplates);
+ expect(template.composed_of).toStrictEqual([...composedOfTemplates, FLEET_COMPONENT_TEMPLATE]);
});
it('adds empty composed_of correctly', () => {
@@ -82,7 +84,7 @@ describe('EPM template', () => {
composedOfTemplates,
templatePriority: 200,
});
- expect(template.composed_of).toStrictEqual(composedOfTemplates);
+ expect(template.composed_of).toStrictEqual([FLEET_COMPONENT_TEMPLATE]);
});
it('adds hidden field correctly', () => {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
index 07d0df021c827..6aa7680395bed 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
@@ -10,13 +10,13 @@ import type { ElasticsearchClient } from 'kibana/server';
import type { Field, Fields } from '../../fields/field';
import type {
RegistryDataStream,
- TemplateRef,
+ IndexTemplateEntry,
IndexTemplate,
IndexTemplateMappings,
} from '../../../../types';
import { appContextService } from '../../../';
import { getRegistryDataStreamAssetBaseName } from '../index';
-import { FINAL_PIPELINE_ID } from '../ingest_pipeline/final_pipeline';
+import { FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME } from '../../../../constants';
interface Properties {
[key: string]: any;
@@ -90,7 +90,11 @@ export function getTemplate({
if (template.template.settings.index.final_pipeline) {
throw new Error(`Error template for ${templateIndexPattern} contains a final_pipeline`);
}
- template.template.settings.index.final_pipeline = FINAL_PIPELINE_ID;
+
+ if (appContextService.getConfig()?.agentIdVerificationEnabled) {
+ // Add fleet global assets
+ template.composed_of = [...(template.composed_of || []), FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME];
+ }
return template;
}
@@ -456,7 +460,7 @@ function getBaseTemplate(
export const updateCurrentWriteIndices = async (
esClient: ElasticsearchClient,
- templates: TemplateRef[]
+ templates: IndexTemplateEntry[]
): Promise => {
if (!templates.length) return;
@@ -471,7 +475,7 @@ function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is Cur
const queryDataStreamsFromTemplates = async (
esClient: ElasticsearchClient,
- templates: TemplateRef[]
+ templates: IndexTemplateEntry[]
): Promise => {
const dataStreamPromises = templates.map((template) => {
return getDataStreams(esClient, template);
@@ -482,7 +486,7 @@ const queryDataStreamsFromTemplates = async (
const getDataStreams = async (
esClient: ElasticsearchClient,
- template: TemplateRef
+ template: IndexTemplateEntry
): Promise => {
const { templateName, indexTemplate } = template;
const { body } = await esClient.indices.getDataStream({ name: `${templateName}-*` });
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
index 65d71ac5fdc17..1bbbb1bb9b6a2 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
@@ -10,10 +10,10 @@ import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } fro
import { MAX_TIME_COMPLETE_INSTALL, ASSETS_SAVED_OBJECT_TYPE } from '../../../../common';
import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
-import { ElasticsearchAssetType } from '../../../types';
import type { AssetReference, Installation, InstallType } from '../../../types';
import { installTemplates } from '../elasticsearch/template/install';
import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/';
+import { getAllTemplateRefs } from '../elasticsearch/template/install';
import { installILMPolicy } from '../elasticsearch/ilm/install';
import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install';
import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
@@ -170,10 +170,7 @@ export async function _installPackage({
installedPkg.attributes.install_version
);
}
- const installedTemplateRefs = installedTemplates.map((template) => ({
- id: template.templateName,
- type: ElasticsearchAssetType.indexTemplate,
- }));
+ const installedTemplateRefs = getAllTemplateRefs(installedTemplates);
// make sure the assets are installed (or didn't error)
if (installKibanaAssetsError) throw installKibanaAssetsError;
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
index 28af2b563da79..6a5968441e634 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
@@ -101,6 +101,8 @@ export async function getPackageSavedObjects(
});
}
+export const getInstallations = getPackageSavedObjects;
+
export async function getPackageInfo(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts
index 608e157017e9b..1f9113590f0f7 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts
@@ -17,6 +17,7 @@ export {
getFile,
getInstallationObject,
getInstallation,
+ getInstallations,
getPackageInfo,
getPackages,
getLimitedPackages,
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
index c6fd9a8f763ab..e00526cbb4ec4 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
@@ -257,8 +257,7 @@ async function installPackageFromRegistry({
const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion);
// try installing the package, if there was an error, call error handler and rethrow
- // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status
- // @ts-ignore
+ // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
return _installPackage({
savedObjectsClient,
esClient,
@@ -334,8 +333,7 @@ async function installPackageByUpload({
version: packageInfo.version,
packageInfo,
});
- // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status
- // @ts-ignore
+ // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
return _installPackage({
savedObjectsClient,
esClient,
@@ -484,17 +482,17 @@ export const saveInstalledEsRefs = async (
return installedAssets;
};
-export const removeAssetsFromInstalledEsByType = async (
+export const removeAssetTypesFromInstalledEs = async (
savedObjectsClient: SavedObjectsClientContract,
pkgName: string,
- assetType: AssetType
+ assetTypes: AssetType[]
) => {
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
const installedAssets = installedPkg?.attributes.installed_es;
if (!installedAssets?.length) return;
- const installedAssetsToSave = installedAssets?.filter(({ id, type }) => {
- return type !== assetType;
- });
+ const installedAssetsToSave = installedAssets?.filter(
+ (asset) => !assetTypes.includes(asset.type)
+ );
return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
installed_es: installedAssetsToSave,
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
index 706f1bbbaaf35..70167d1156a66 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
@@ -89,13 +89,18 @@ function deleteKibanaAssets(
});
}
-function deleteESAssets(installedObjects: EsAssetReference[], esClient: ElasticsearchClient) {
+function deleteESAssets(
+ installedObjects: EsAssetReference[],
+ esClient: ElasticsearchClient
+): Array> {
return installedObjects.map(async ({ id, type }) => {
const assetType = type as AssetType;
if (assetType === ElasticsearchAssetType.ingestPipeline) {
return deletePipeline(esClient, id);
} else if (assetType === ElasticsearchAssetType.indexTemplate) {
- return deleteTemplate(esClient, id);
+ return deleteIndexTemplate(esClient, id);
+ } else if (assetType === ElasticsearchAssetType.componentTemplate) {
+ return deleteComponentTemplate(esClient, id);
} else if (assetType === ElasticsearchAssetType.transform) {
return deleteTransforms(esClient, [id]);
} else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) {
@@ -111,13 +116,30 @@ async function deleteAssets(
) {
const logger = appContextService.getLogger();
- const deletePromises: Array> = [
- ...deleteESAssets(installedEs, esClient),
- ...deleteKibanaAssets(installedKibana, savedObjectsClient),
- ];
+ // must delete index templates first, or component templates which reference them cannot be deleted
+ // separate the assets into Index Templates and other assets
+ type Tuple = [EsAssetReference[], EsAssetReference[]];
+ const [indexTemplates, otherAssets] = installedEs.reduce(
+ ([indexAssetTypes, otherAssetTypes], asset) => {
+ if (asset.type === ElasticsearchAssetType.indexTemplate) {
+ indexAssetTypes.push(asset);
+ } else {
+ otherAssetTypes.push(asset);
+ }
+
+ return [indexAssetTypes, otherAssetTypes];
+ },
+ [[], []]
+ );
try {
- await Promise.all(deletePromises);
+ // must delete index templates first
+ await Promise.all(deleteESAssets(indexTemplates, esClient));
+ // then the other asset types
+ await Promise.all([
+ ...deleteESAssets(otherAssets, esClient),
+ ...deleteKibanaAssets(installedKibana, savedObjectsClient),
+ ]);
} catch (err) {
// in the rollback case, partial installs are likely, so missing assets are not an error
if (!savedObjectsClient.errors.isNotFoundError(err)) {
@@ -126,13 +148,24 @@ async function deleteAssets(
}
}
-async function deleteTemplate(esClient: ElasticsearchClient, name: string): Promise {
+async function deleteIndexTemplate(esClient: ElasticsearchClient, name: string): Promise {
// '*' shouldn't ever appear here, but it still would delete all templates
if (name && name !== '*') {
try {
await esClient.indices.deleteIndexTemplate({ name }, { ignore: [404] });
} catch {
- throw new Error(`error deleting template ${name}`);
+ throw new Error(`error deleting index template ${name}`);
+ }
+ }
+}
+
+async function deleteComponentTemplate(esClient: ElasticsearchClient, name: string): Promise {
+ // '*' shouldn't ever appear here, but it still would delete all templates
+ if (name && name !== '*') {
+ try {
+ await esClient.cluster.deleteComponentTemplate({ name }, { ignore: [404] });
+ } catch (error) {
+ throw new Error(`error deleting component template ${name}`);
}
}
}
diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts
index 0c7b086f78fdf..8c6bc7eca0401 100644
--- a/x-pack/plugins/fleet/server/services/output.ts
+++ b/x-pack/plugins/fleet/server/services/output.ts
@@ -9,10 +9,9 @@ import type { SavedObjectsClientContract } from 'src/core/server';
import type { NewOutput, Output, OutputSOAttributes } from '../types';
import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants';
-import { decodeCloudId } from '../../common';
+import { decodeCloudId, normalizeHostsForAgents } from '../../common';
import { appContextService } from './app_context';
-import { normalizeHostsForAgents } from './hosts_utils';
const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE;
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts
index a8be94ca61c0a..e016fafe5459d 100644
--- a/x-pack/plugins/fleet/server/services/preconfiguration.ts
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts
@@ -108,7 +108,7 @@ export async function ensurePreconfiguredPackagesAndPolicies(
policies.map(async (preconfiguredAgentPolicy) => {
if (preconfiguredAgentPolicy.id) {
// Check to see if a preconfigured policy with the same preconfiguration id was already deleted by the user
- const preconfigurationId = String(preconfiguredAgentPolicy.id);
+ const preconfigurationId = preconfiguredAgentPolicy.id.toString();
const searchParams = {
searchFields: ['id'],
search: escapeSearchQueryPhrase(preconfigurationId),
diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts
index 226fbb29467c2..26d581f32d9a2 100644
--- a/x-pack/plugins/fleet/server/services/settings.ts
+++ b/x-pack/plugins/fleet/server/services/settings.ts
@@ -8,11 +8,14 @@
import Boom from '@hapi/boom';
import type { SavedObjectsClientContract } from 'kibana/server';
-import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common';
+import {
+ decodeCloudId,
+ GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ normalizeHostsForAgents,
+} from '../../common';
import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common';
import { appContextService } from './app_context';
-import { normalizeHostsForAgents } from './hosts_utils';
export async function getSettings(soClient: SavedObjectsClientContract): Promise {
const res = await soClient.find({
diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts
index 45805bb066c3b..cfef04846d92e 100644
--- a/x-pack/plugins/fleet/server/services/setup.ts
+++ b/x-pack/plugins/fleet/server/services/setup.ts
@@ -24,7 +24,10 @@ import { awaitIfPending } from './setup_utils';
import { ensureAgentActionPolicyChangeExists } from './agents';
import { awaitIfFleetServerSetupPending } from './fleet_server';
import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install';
+import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install';
+import { getInstallations, installPackage } from './epm/packages';
import { isPackageInstalled } from './epm/packages/install';
+import { pkgToPkgKey } from './epm/registry';
export interface SetupStatus {
isInitialized: boolean;
@@ -47,9 +50,10 @@ async function createSetupSideEffects(
settingsService.settingsSetup(soClient),
]);
- await ensureFleetFinalPipelineIsInstalled(esClient);
-
await awaitIfFleetServerSetupPending();
+ if (appContextService.getConfig()?.agentIdVerificationEnabled) {
+ await ensureFleetGlobalEsAssets(soClient, esClient);
+ }
const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } =
appContextService.getConfig() ?? {};
@@ -95,6 +99,49 @@ async function createSetupSideEffects(
};
}
+/**
+ * Ensure ES assets shared by all Fleet index template are installed
+ */
+export async function ensureFleetGlobalEsAssets(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient
+) {
+ const logger = appContextService.getLogger();
+ // Ensure Global Fleet ES assets are installed
+ const globalAssetsRes = await Promise.all([
+ ensureDefaultComponentTemplate(esClient),
+ ensureFleetFinalPipelineIsInstalled(esClient),
+ ]);
+
+ if (globalAssetsRes.some((asset) => asset.isCreated)) {
+ // Update existing index template
+ const packages = await getInstallations(soClient);
+
+ await Promise.all(
+ packages.saved_objects.map(async ({ attributes: installation }) => {
+ if (installation.install_source !== 'registry') {
+ logger.error(
+ `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets`
+ );
+ return;
+ }
+ await installPackage({
+ installSource: installation.install_source,
+ savedObjectsClient: soClient,
+ pkgkey: pkgToPkgKey({ name: installation.name, version: installation.version }),
+ esClient,
+ // Force install the pacakge will update the index template and the datastream write indices
+ force: true,
+ }).catch((err) => {
+ logger.error(
+ `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets: ${err.message}`
+ );
+ });
+ })
+ );
+ }
+}
+
export async function ensureDefaultEnrollmentAPIKeysExists(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx
index 8927676976457..0c08a09e76f4e 100644
--- a/x-pack/plugins/fleet/server/types/index.tsx
+++ b/x-pack/plugins/fleet/server/types/index.tsx
@@ -63,7 +63,7 @@ export {
IndexTemplate,
RegistrySearchResults,
RegistrySearchResult,
- TemplateRef,
+ IndexTemplateEntry,
IndexTemplateMappings,
Settings,
SettingsSOAttributes,
diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
index db551b25e9ebb..48aea1b5cbcc4 100644
--- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
@@ -16,6 +16,7 @@ export const AgentPolicyBaseSchema = {
namespace: NamespaceSchema,
description: schema.maybe(schema.string()),
is_managed: schema.maybe(schema.boolean()),
+ unenroll_timeout: schema.maybe(schema.number({ min: 1 })),
monitoring_enabled: schema.maybe(
schema.arrayOf(
schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)])
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
index 6254a6512efb5..9595009347259 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
@@ -314,89 +314,99 @@ exports[`extend index management ilm summary extension should return extension w
- illegal_argument_exception
- :
- setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
-
-
-
+ illegal_argument_exception
+ :
+ setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
+
-
-
- }
- closePopover={[Function]}
- display="inlineBlock"
- hasArrow={true}
- id="stackPopover"
- isOpen={false}
- ownFocus={true}
- panelPaddingSize="m"
- >
-
-
+
+
+
+ }
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="stackPopover"
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="m"
>
-
-
-
-
-
-
- Stack trace
-
-
-
-
-
-
-
+
+
+ Stack trace
+
+
+
+
+
+
+
+
+
-
+
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
index 556ac35d0565e..4d2b47c8a6039 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
@@ -58,16 +58,16 @@ exports[`policy table should show empty state when there are not any policies 1`
data-euiicon-type="managementApp"
/>
+
+ Create your first index lifecycle policy
+
-
- Create your first index lifecycle policy
-
@@ -82,9 +82,6 @@ exports[`policy table should show empty state when there are not any policies 1`
-
new Promise(setImmediate);
+
const status = (rendered, row = 0) => {
rendered.update();
return findTestSubject(rendered, 'indexTableCell-status')
@@ -76,39 +80,54 @@ const status = (rendered, row = 0) => {
const snapshot = (rendered) => {
expect(rendered).toMatchSnapshot();
};
+
const openMenuAndClickButton = (rendered, rowIndex, buttonIndex) => {
+ // Select a row.
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(rowIndex).simulate('change', { target: { checked: true } });
rendered.update();
+
+ // Click the bulk actions button to open the context menu.
const actionButton = findTestSubject(rendered, 'indexActionsContextMenuButton');
actionButton.simulate('click');
rendered.update();
+
+ // Click an action in the context menu.
const contextMenuButtons = findTestSubject(rendered, 'indexTableContextMenuButton');
contextMenuButtons.at(buttonIndex).simulate('click');
+ rendered.update();
};
-const testEditor = (buttonIndex, rowIndex = 0) => {
- const rendered = mountWithIntl(component);
+
+const testEditor = (rendered, buttonIndex, rowIndex = 0) => {
openMenuAndClickButton(rendered, rowIndex, buttonIndex);
rendered.update();
snapshot(findTestSubject(rendered, 'detailPanelTabSelected').text());
};
-const testAction = (buttonIndex, done, rowIndex = 0) => {
- const rendered = mountWithIntl(component);
- let count = 0;
+
+const testAction = (rendered, buttonIndex, rowIndex = 0) => {
+ // This is leaking some implementation details about how Redux works. Not sure exactly what's going on
+ // but it looks like we're aware of how many Redux actions are dispatched in response to user interaction,
+ // so we "time" our assertion based on how many Redux actions we observe. This is brittle because it
+ // depends upon how our UI is architected, which will affect how many actions are dispatched.
+ // Expect this to break when we rearchitect the UI.
+ let dispatchedActionsCount = 0;
store.subscribe(() => {
- if (count > 1) {
+ if (dispatchedActionsCount === 1) {
+ // Take snapshot of final state.
snapshot(status(rendered, rowIndex));
- done();
}
- count++;
+ dispatchedActionsCount++;
});
- expect.assertions(2);
+
openMenuAndClickButton(rendered, rowIndex, buttonIndex);
+ // take snapshot of initial state.
snapshot(status(rendered, rowIndex));
};
+
const names = (rendered) => {
return findTestSubject(rendered, 'indexTableIndexNameLink');
};
+
const namesText = (rendered) => {
return names(rendered).map((button) => button.text());
};
@@ -142,23 +161,28 @@ describe('index table', () => {
);
+
store.dispatch(loadIndicesSuccess({ indices }));
server = sinon.fakeServer.create();
+
server.respondWith(`${API_BASE_PATH}/indices`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(indices),
]);
+
server.respondWith([
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({ acknowledged: true }),
]);
+
server.respondWith(`${API_BASE_PATH}/indices/reload`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(indices),
]);
+
server.respondImmediately = true;
});
afterEach(() => {
@@ -168,83 +192,124 @@ describe('index table', () => {
server.restore();
});
- test('should change pages when a pagination link is clicked on', () => {
+ test('should change pages when a pagination link is clicked on', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
snapshot(namesText(rendered));
+
const pagingButtons = rendered.find('.euiPaginationButton');
pagingButtons.at(2).simulate('click');
- rendered.update();
snapshot(namesText(rendered));
});
- test('should show more when per page value is increased', () => {
+
+ test('should show more when per page value is increased', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button');
perPageButton.simulate('click');
rendered.update();
+
const fiftyButton = rendered.find('.euiContextMenuItem').at(1);
fiftyButton.simulate('click');
rendered.update();
expect(namesText(rendered).length).toBe(50);
});
- test('should show the Actions menu button only when at least one row is selected', () => {
+
+ test('should show the Actions menu button only when at least one row is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
let button = findTestSubject(rendered, 'indexTableContextMenuButton');
expect(button.length).toEqual(0);
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
rendered.update();
button = findTestSubject(rendered, 'indexActionsContextMenuButton');
expect(button.length).toEqual(1);
});
- test('should update the Actions menu button text when more than one row is selected', () => {
+
+ test('should update the Actions menu button text when more than one row is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
let button = findTestSubject(rendered, 'indexTableContextMenuButton');
expect(button.length).toEqual(0);
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
rendered.update();
button = findTestSubject(rendered, 'indexActionsContextMenuButton');
expect(button.text()).toEqual('Manage index');
+
checkboxes.at(1).simulate('change', { target: { checked: true } });
rendered.update();
button = findTestSubject(rendered, 'indexActionsContextMenuButton');
expect(button.text()).toEqual('Manage 2 indices');
});
- test('should show system indices only when the switch is turned on', () => {
+
+ test('should show system indices only when the switch is turned on', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
const switchControl = rendered.find('.euiSwitch__button');
switchControl.simulate('click');
snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
});
- test('should filter based on content of search input', () => {
+
+ test('should filter based on content of search input', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const searchInput = rendered.find('.euiFieldSearch').first();
searchInput.instance().value = 'testy0';
searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 });
rendered.update();
snapshot(namesText(rendered));
});
- test('should sort when header is clicked', () => {
+
+ test('should sort when header is clicked', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const nameHeader = findTestSubject(rendered, 'indexTableHeaderCell-name').find('button');
nameHeader.simulate('click');
rendered.update();
snapshot(namesText(rendered));
+
nameHeader.simulate('click');
rendered.update();
snapshot(namesText(rendered));
});
- test('should open the index detail slideout when the index name is clicked', () => {
+
+ test('should open the index detail slideout when the index name is clicked', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(0);
+
const indexNameLink = names(rendered).at(0);
indexNameLink.simulate('click');
rendered.update();
expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(1);
});
- test('should show the right context menu options when one index is selected and open', () => {
+
+ test('should show the right context menu options when one index is selected and open', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
rendered.update();
@@ -253,8 +318,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when one index is selected and closed', () => {
+
+ test('should show the right context menu options when one index is selected and closed', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(1).simulate('change', { target: { checked: true } });
rendered.update();
@@ -263,8 +332,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when one open and one closed index is selected', () => {
+
+ test('should show the right context menu options when one open and one closed index is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
checkboxes.at(1).simulate('change', { target: { checked: true } });
@@ -274,8 +347,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when more than one open index is selected', () => {
+
+ test('should show the right context menu options when more than one open index is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
checkboxes.at(2).simulate('change', { target: { checked: true } });
@@ -285,8 +362,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when more than one closed index is selected', () => {
+
+ test('should show the right context menu options when more than one closed index is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(1).simulate('change', { target: { checked: true } });
checkboxes.at(3).simulate('change', { target: { checked: true } });
@@ -296,37 +377,57 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('flush button works from context menu', (done) => {
- testAction(8, done);
+
+ test('flush button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testAction(rendered, 8);
});
- test('clear cache button works from context menu', (done) => {
- testAction(7, done);
+
+ test('clear cache button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testAction(rendered, 7);
});
- test('refresh button works from context menu', (done) => {
- testAction(6, done);
+
+ test('refresh button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testAction(rendered, 6);
});
- test('force merge button works from context menu', (done) => {
+
+ test('force merge button works from context menu', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const rowIndex = 0;
openMenuAndClickButton(rendered, rowIndex, 5);
snapshot(status(rendered, rowIndex));
expect(rendered.find('.euiModal').length).toBe(1);
+
let count = 0;
store.subscribe(() => {
- if (count > 1) {
+ if (count === 1) {
snapshot(status(rendered, rowIndex));
expect(rendered.find('.euiModal').length).toBe(0);
- done();
}
count++;
});
+
const confirmButton = findTestSubject(rendered, 'confirmModalConfirmButton');
confirmButton.simulate('click');
snapshot(status(rendered, rowIndex));
});
- // Commenting the following 2 tests as it works in the browser (status changes to "closed" or "open") but the
- // snapshot say the contrary. Need to be investigated.
- test('close index button works from context menu', (done) => {
+
+ test('close index button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const modifiedIndices = indices.map((index) => {
return {
...index,
@@ -339,32 +440,56 @@ describe('index table', () => {
{ 'Content-Type': 'application/json' },
JSON.stringify(modifiedIndices),
]);
- testAction(4, done);
+
+ testAction(rendered, 4);
});
- test('open index button works from context menu', (done) => {
+
+ test('open index button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const modifiedIndices = indices.map((index) => {
return {
...index,
status: index.name === 'testy1' ? 'open' : index.status,
};
});
+
server.respondWith(`${API_BASE_PATH}/indices/reload`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(modifiedIndices),
]);
- testAction(3, done, 1);
+
+ testAction(rendered, 3, 1);
});
- test('show settings button works from context menu', () => {
- testEditor(0);
+
+ test('show settings button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 0);
});
- test('show mappings button works from context menu', () => {
- testEditor(1);
+
+ test('show mappings button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 1);
});
- test('show stats button works from context menu', () => {
- testEditor(2);
+
+ test('show stats button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 2);
});
- test('edit index button works from context menu', () => {
- testEditor(3);
+
+ test('edit index button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 3);
});
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
index 8c8f7e5789925..dee15f2ae3a45 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
@@ -165,8 +165,10 @@ describe(' ', () => {
const { exists, find } = testBed;
expect(exists('componentTemplatesLoadError')).toBe(true);
+ // The text here looks weird because the child elements' text values (title and description)
+ // are concatenated when we retrive the error element's text value.
expect(find('componentTemplatesLoadError').text()).toContain(
- 'Unable to load component templates. Try again.'
+ 'Error loading component templatesInternal server error'
);
});
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
index 2bb240e6b6ae1..77668f7d55072 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
@@ -13,8 +13,13 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { ScopedHistory } from 'kibana/public';
import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
-import { attemptToURIDecode } from '../../../../shared_imports';
-import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports';
+import {
+ APP_WRAPPER_CLASS,
+ PageLoading,
+ PageError,
+ attemptToURIDecode,
+} from '../../../../shared_imports';
+import { ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports';
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
import { useComponentTemplatesContext } from '../component_templates_context';
import {
@@ -24,7 +29,6 @@ import {
} from '../component_template_details';
import { EmptyPrompt } from './empty_prompt';
import { ComponentTable } from './table';
-import { LoadError } from './error';
import { ComponentTemplatesDeleteModal } from './delete_modal';
interface Props {
@@ -138,18 +142,20 @@ export const ComponentTemplateList: React.FunctionComponent = ({
}
}, [componentTemplateName, removeContentFromGlobalFlyout]);
- let content: React.ReactNode;
-
if (isLoading) {
- content = (
-
+ return (
+
-
+
);
- } else if (data?.length) {
+ }
+
+ let content: React.ReactNode;
+
+ if (data?.length) {
content = (
<>
@@ -183,11 +189,22 @@ export const ComponentTemplateList: React.FunctionComponent = ({
} else if (data && data.length === 0) {
content = ;
} else if (error) {
- content = ;
+ content = (
+
+ }
+ error={error}
+ data-test-subj="componentTemplatesLoadError"
+ />
+ );
}
return (
-
+
{content}
{/* delete modal */}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
deleted file mode 100644
index 9fd0031fe8778..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { FunctionComponent } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiLink, EuiCallOut } from '@elastic/eui';
-
-export interface Props {
- onReloadClick: () => void;
-}
-
-export const LoadError: FunctionComponent
= ({ onReloadClick }) => {
- return (
-
-
-
- ),
- }}
- />
- }
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
index a0f6dc4b59fe7..eecb56768df9a 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
@@ -9,10 +9,10 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { FunctionComponent } from 'react';
import {
- SectionError,
+ PageLoading,
+ PageError,
useAuthorizationContext,
WithPrivileges,
- SectionLoading,
NotAuthorizedSection,
} from '../shared_imports';
import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants';
@@ -26,7 +26,7 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({
if (apiError) {
return (
- {
if (isLoading) {
return (
-
+
-
+
);
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
index b87b043c924a6..d19c500c3622a 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
@@ -10,7 +10,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { SectionLoading, attemptToURIDecode } from '../../shared_imports';
+import { PageLoading, attemptToURIDecode } from '../../shared_imports';
import { useComponentTemplatesContext } from '../../component_templates_context';
import { ComponentTemplateCreate } from '../component_template_create';
@@ -30,7 +30,8 @@ export const ComponentTemplateClone: FunctionComponent {
if (error && !isLoading) {
- toasts.addError(error, {
+ // Toasts expects a generic Error object, which is typed as having a required name property.
+ toasts.addError({ ...error, name: '' } as Error, {
title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', {
defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`,
values: { sourceComponentTemplateName },
@@ -42,12 +43,12 @@ export const ComponentTemplateClone: FunctionComponent
+
-
+
);
} else {
// We still show the create form (unpopulated) even if we were not able to load the
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
index 5163c75bdbadd..8fe2c193daa0c 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
@@ -8,7 +8,7 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { ComponentTemplateDeserialized } from '../../shared_imports';
import { useComponentTemplatesContext } from '../../component_templates_context';
@@ -59,27 +59,28 @@ export const ComponentTemplateCreate: React.FunctionComponent
-
-
-
+
+
-
-
-
-
-
-
-
-
+
+ }
+ bottomBorder
+ />
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx
index 809fac980069f..6ac831b5dacce 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx
@@ -8,13 +8,15 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
+import { EuiPageContentBody, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { useComponentTemplatesContext } from '../../component_templates_context';
import {
ComponentTemplateDeserialized,
- SectionLoading,
+ PageLoading,
+ PageError,
attemptToURIDecode,
+ Error,
} from '../../shared_imports';
import { ComponentTemplateForm } from '../component_template_form';
@@ -65,64 +67,57 @@ export const ComponentTemplateEdit: React.FunctionComponent
+ return (
+
-
- );
- } else if (error) {
- content = (
- <>
-
- }
- color="danger"
- iconType="alert"
- data-test-subj="loadComponentTemplateError"
- >
- {error.message}
-
-
- >
+
);
- } else if (componentTemplate) {
- content = (
-
+ }
+ error={error as Error}
+ data-test-subj="loadComponentTemplateError"
/>
);
}
return (
-
-
-
-
+
+
-
-
-
- {content}
-
-
+
+ }
+ bottomBorder
+ />
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts
index 75c68e71996b8..6bf6d204fd9a5 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts
@@ -10,7 +10,6 @@ import {
ComponentTemplateListItem,
ComponentTemplateDeserialized,
ComponentTemplateSerialized,
- Error,
} from '../shared_imports';
import {
UIM_COMPONENT_TEMPLATE_DELETE_MANY,
@@ -26,7 +25,7 @@ export const getApi = (
trackMetric: (type: UiCounterMetricType, eventName: string) => void
) => {
function useLoadComponentTemplates() {
- return useRequest({
+ return useRequest({
path: `${apiBasePath}/component_templates`,
method: 'get',
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts
index 64b2e6b47e5d9..a7056e27b5cad 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts
@@ -14,6 +14,7 @@ import {
SendRequestResponse,
sendRequest as _sendRequest,
useRequest as _useRequest,
+ Error,
} from '../shared_imports';
export type UseRequestHook = (
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
index afc7aed874387..15528f5b4e8e5 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts
@@ -12,10 +12,12 @@ export {
SendRequestResponse,
sendRequest,
useRequest,
- SectionLoading,
WithPrivileges,
AuthorizationProvider,
SectionError,
+ SectionLoading,
+ PageLoading,
+ PageError,
Error,
useAuthorizationContext,
NotAuthorizedSection,
diff --git a/x-pack/plugins/index_management/public/application/components/index.ts b/x-pack/plugins/index_management/public/application/components/index.ts
index f5c58e5b45ebd..eeba6e16b543c 100644
--- a/x-pack/plugins/index_management/public/application/components/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/index.ts
@@ -6,9 +6,7 @@
*/
export { SectionError, Error } from './section_error';
-export { SectionLoading } from './section_loading';
export { NoMatch } from './no_match';
-export { PageErrorForbidden } from './page_error';
export { TemplateDeleteModal } from './template_delete_modal';
export { TemplateForm } from './template_form';
export { DataHealth } from './data_health';
diff --git a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx
deleted file mode 100644
index e22b180881ed5..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export function PageErrorForbidden() {
- return (
-
-
-
-
- }
- />
-
- );
-}
diff --git a/x-pack/plugins/index_management/public/application/components/section_loading.tsx b/x-pack/plugins/index_management/public/application/components/section_loading.tsx
deleted file mode 100644
index 3c31744dee398..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/section_loading.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui';
-
-interface Props {
- children: React.ReactNode;
-}
-
-export const SectionLoading: React.FunctionComponent = ({ children }) => {
- return (
- }
- body={{children} }
- data-test-subj="sectionLoading"
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index 54160141827d0..4ccd77d275a94 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -8,7 +8,7 @@
import React, { useState, useCallback, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiSpacer, EuiButton } from '@elastic/eui';
+import { EuiSpacer, EuiButton, EuiPageHeader } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
@@ -292,7 +292,7 @@ export const TemplateForm = ({
return (
<>
{/* Form header */}
- {title}
+ {title}} bottomBorder />
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
index a9258c6a3b10b..3d5f56c08f8e1 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
@@ -24,8 +24,8 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { reactRouterNavigate } from '../../../../../shared_imports';
-import { SectionLoading, SectionError, Error, DataHealth } from '../../../../components';
+import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports';
+import { SectionError, Error, DataHealth } from '../../../../components';
import { useLoadDataStream } from '../../../../services/api';
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
import { humanizeTimeStamp } from '../humanize_time_stamp';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
index 131dc2662bc1c..7bd7c163837d8 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
@@ -16,18 +16,22 @@ import {
EuiText,
EuiIconTip,
EuiSpacer,
+ EuiPageContent,
EuiEmptyPrompt,
EuiLink,
} from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import {
+ PageLoading,
+ PageError,
+ Error,
reactRouterNavigate,
extractQueryParams,
attemptToURIDecode,
+ APP_WRAPPER_CLASS,
} from '../../../../shared_imports';
import { useAppContext } from '../../../app_context';
-import { SectionError, SectionLoading, Error } from '../../../components';
import { useLoadDataStreams } from '../../../services/api';
import { documentationService } from '../../../services/documentation';
import { Section } from '../home';
@@ -166,16 +170,16 @@ export const DataStreamList: React.FunctionComponent
+
-
+
);
} else if (error) {
content = (
-
);
- } else if (Array.isArray(dataStreams) && dataStreams.length > 0) {
- activateHiddenFilter(isSelectedDataStreamHidden(dataStreams, decodedDataStreamName));
+ } else {
+ activateHiddenFilter(isSelectedDataStreamHidden(dataStreams!, decodedDataStreamName));
content = (
- <>
+
{renderHeader()}
@@ -270,12 +274,12 @@ export const DataStreamList: React.FunctionComponent
- >
+
);
}
return (
-
+
{content}
{/*
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx
index ac46b5dbd256b..fc68ca33e9536 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx
@@ -8,12 +8,13 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
+import { APP_WRAPPER_CLASS } from '../../../../shared_imports';
import { DetailPanel } from './detail_panel';
import { IndexTable } from './index_table';
export const IndexList: React.FunctionComponent
= ({ history }) => {
return (
-
+
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
index f488290692e7e..0a407927e3466 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
@@ -19,7 +19,7 @@ import {
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
- EuiLoadingSpinner,
+ EuiPageContent,
EuiScreenReaderOnly,
EuiSpacer,
EuiSearchBar,
@@ -37,13 +37,18 @@ import {
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
-import { reactRouterNavigate, attemptToURIDecode } from '../../../../../shared_imports';
+import {
+ PageLoading,
+ PageError,
+ reactRouterNavigate,
+ attemptToURIDecode,
+} from '../../../../../shared_imports';
import { REFRESH_RATE_INDEX_LIST } from '../../../../constants';
import { getDataStreamDetailsLink } from '../../../../services/routing';
import { documentationService } from '../../../../services/documentation';
import { AppContextConsumer } from '../../../../app_context';
import { renderBadges } from '../../../../lib/render_badges';
-import { NoMatch, PageErrorForbidden, DataHealth } from '../../../../components';
+import { NoMatch, DataHealth } from '../../../../components';
import { IndexActionsContextMenu } from '../index_actions_context_menu';
const HEADERS = {
@@ -332,42 +337,6 @@ export class IndexTable extends Component {
});
}
- renderError() {
- const { indicesError } = this.props;
-
- const data = indicesError.body ? indicesError.body : indicesError;
-
- const { error: errorString, cause, message } = data;
-
- return (
-
-
- }
- color="danger"
- iconType="alert"
- >
- {message || errorString}
- {cause && (
-
-
-
- {cause.map((message, i) => (
- {message}
- ))}
-
-
- )}
-
-
-
- );
- }
-
renderBanners(extensionsService) {
const { allIndices = [], filterChanged } = this.props;
return extensionsService.banners.map((bannerExtension, i) => {
@@ -470,37 +439,71 @@ export class IndexTable extends Component {
} = this.props;
const { includeHiddenIndices } = this.readURLParams();
+ const hasContent = !indicesLoading && !indicesError;
- let emptyState;
+ if (!hasContent) {
+ const renderNoContent = () => {
+ if (indicesLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (indicesError) {
+ if (indicesError.status === 403) {
+ return (
+
+ }
+ />
+ );
+ }
- if (indicesLoading) {
- emptyState = (
-
-
-
-
-
- );
- }
+ return (
+
+ }
+ error={indicesError.body}
+ />
+ );
+ }
+ };
- if (!indicesLoading && !indicesError) {
- emptyState =
;
+ return (
+
+ {renderNoContent()}
+
+ );
}
const { selectedIndicesMap } = this.state;
const atLeastOneItemSelected = Object.keys(selectedIndicesMap).length > 0;
- if (indicesError && indicesError.status === 403) {
- return
;
- }
-
return (
{({ services }) => {
const { extensionsService } = services;
return (
-
+
@@ -557,8 +560,6 @@ export class IndexTable extends Component {
{this.renderBanners(extensionsService)}
- {indicesError && this.renderError()}
-
{atLeastOneItemSelected ? (
@@ -665,13 +666,13 @@ export class IndexTable extends Component {
) : (
- emptyState
+
)}
{indices.length > 0 ? this.renderPager() : null}
-
+
);
}}
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
index e61362efb8c99..1a82cb3bfbdd1 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
@@ -33,8 +33,8 @@ import {
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB,
} from '../../../../../../common/constants';
-import { UseRequestResponse } from '../../../../../shared_imports';
-import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components';
+import { SectionLoading, UseRequestResponse } from '../../../../../shared_imports';
+import { TemplateDeleteModal, SectionError, Error } from '../../../../components';
import { useLoadIndexTemplate } from '../../../../services/api';
import { useServices } from '../../../../app_context';
import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index b8b5a8e3c7d1a..57f18134be5d6 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, useState, useEffect, useMemo } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -24,13 +24,14 @@ import {
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
import { TemplateListItem } from '../../../../../common';
-import { attemptToURIDecode } from '../../../../shared_imports';
import {
- SectionError,
- SectionLoading,
- Error,
- LegacyIndexTemplatesDeprecation,
-} from '../../../components';
+ APP_WRAPPER_CLASS,
+ PageLoading,
+ PageError,
+ attemptToURIDecode,
+ reactRouterNavigate,
+} from '../../../../shared_imports';
+import { LegacyIndexTemplatesDeprecation } from '../../../components';
import { useLoadIndexTemplates } from '../../../services/api';
import { documentationService } from '../../../services/documentation';
import { useServices } from '../../../app_context';
@@ -130,7 +131,8 @@ export const TemplateList: React.FunctionComponent (
-
+ // flex-grow: 0 is needed here because the parent element is a flex column and the header would otherwise expand.
+
);
- const renderContent = () => {
- if (isLoading) {
- return (
-
+ // Track this component mounted.
+ useEffect(() => {
+ uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD);
+ }, [uiMetricService]);
+
+ let content;
+
+ if (isLoading) {
+ content = (
+
+
+
+ );
+ } else if (error) {
+ content = (
+
-
- );
- } else if (error) {
- return (
-
+ );
+ } else if (!hasTemplates) {
+ content = (
+
- }
- error={error as Error}
- />
- );
- } else if (!hasTemplates) {
- return (
-
+
+ }
+ body={
+ <>
+
-
- }
- data-test-subj="emptyPrompt"
- />
- );
- } else {
- return (
-
- {/* Header */}
- {renderHeader()}
+
+ >
+ }
+ actions={
+
+
+
+ }
+ data-test-subj="emptyPrompt"
+ />
+ );
+ } else {
+ content = (
+ <>
+ {/* Header */}
+ {renderHeader()}
- {/* Composable index templates table */}
- {renderTemplatesTable()}
+ {/* Composable index templates table */}
+ {renderTemplatesTable()}
- {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */}
- {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()}
-
- );
- }
- };
+ {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */}
+ {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()}
- // Track component loaded
- useEffect(() => {
- uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD);
- }, [uiMetricService]);
+ {isTemplateDetailsVisible && (
+
+ )}
+ >
+ );
+ }
return (
-
- {renderContent()}
-
- {isTemplateDetailsVisible && (
-
- )}
+
+ {content}
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
index 36bff298e345b..32c84bc3b15f1 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
@@ -8,11 +8,12 @@
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
+import { PageLoading, PageError, Error } from '../../../shared_imports';
import { TemplateDeserialized } from '../../../../common';
-import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
+import { TemplateForm } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
import { getTemplateDetailsLink } from '../../services/routing';
import { saveTemplate, useLoadIndexTemplate } from '../../services/api';
@@ -62,24 +63,22 @@ export const TemplateClone: React.FunctionComponent
{
breadcrumbService.setBreadcrumbs('templateClone');
}, []);
if (isLoading) {
- content = (
-
+ return (
+
-
+
);
} else if (templateToCloneError) {
- content = (
-
);
- } else if (templateToClone) {
- const templateData = {
- ...templateToClone,
- name: `${decodedTemplateName}-copy`,
- } as TemplateDeserialized;
+ }
+
+ const templateData = {
+ ...templateToClone,
+ name: `${decodedTemplateName}-copy`,
+ } as TemplateDeserialized;
- content = (
+ return (
+
-
-
-
-
+
}
defaultValue={templateData}
onSave={onSave}
@@ -117,12 +114,6 @@ export const TemplateClone: React.FunctionComponent
- );
- }
-
- return (
-
- {content}
-
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
index 310807aeef38f..6eba112b11939 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
@@ -8,7 +8,7 @@
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody } from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { parse } from 'query-string';
import { ScopedHistory } from 'kibana/public';
@@ -52,34 +52,28 @@ export const TemplateCreate: React.FunctionComponent = ({ h
}, []);
return (
-
-
-
-
- {isLegacy ? (
-
- ) : (
-
- )}
-
-
- }
- onSave={onSave}
- isSaving={isSaving}
- saveError={saveError}
- clearSaveError={clearSaveError}
- isLegacy={isLegacy}
- history={history as ScopedHistory}
- />
-
-
+
+
+ ) : (
+
+ )
+ }
+ onSave={onSave}
+ isSaving={isSaving}
+ saveError={saveError}
+ clearSaveError={clearSaveError}
+ isLegacy={isLegacy}
+ history={history as ScopedHistory}
+ />
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
index f4ffe97931a24..ff6909d4666f8 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
@@ -7,16 +7,17 @@
import React, { useEffect, useState, Fragment } from 'react';
import { RouteComponentProps } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
-import { attemptToURIDecode } from '../../../shared_imports';
+import { PageError, PageLoading, attemptToURIDecode, Error } from '../../../shared_imports';
import { breadcrumbService } from '../../services/breadcrumbs';
import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
-import { SectionLoading, SectionError, TemplateForm, Error } from '../../components';
+import { TemplateForm } from '../../components';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
interface MatchParams {
@@ -62,27 +63,27 @@ export const TemplateEdit: React.FunctionComponent
+ return (
+
-
+
);
} else if (error) {
- content = (
-
}
- error={error as Error}
+ error={error}
data-test-subj="sectionError"
/>
);
@@ -91,80 +92,75 @@ export const TemplateEdit: React.FunctionComponent
}
- color="danger"
- iconType="alert"
+ error={
+ {
+ message: i18n.translate(
+ 'xpack.idxMgmt.templateEdit.managedTemplateWarningDescription',
+ {
+ defaultMessage: 'Managed templates are critical for internal operations.',
+ }
+ ),
+ } as Error
+ }
data-test-subj="systemTemplateEditCallout"
- >
-
-
+ />
);
- } else {
- content = (
+ }
+ }
+
+ return (
+
+ {isSystemTemplate && (
- {isSystemTemplate && (
-
-
- }
- color="danger"
- iconType="alert"
- data-test-subj="systemTemplateEditCallout"
- >
-
-
-
-
- )}
-
-
-
-
-
+
}
- defaultValue={template}
- onSave={onSave}
- isSaving={isSaving}
- saveError={saveError}
- clearSaveError={clearSaveError}
- isEditing={true}
- isLegacy={isLegacy}
- history={history as ScopedHistory}
- />
+ color="danger"
+ iconType="alert"
+ data-test-subj="systemTemplateEditCallout"
+ >
+
+
+
- );
- }
- }
+ )}
- return (
-
- {content}
-
+
+ }
+ defaultValue={template!}
+ onSave={onSave}
+ isSaving={isSaving}
+ saveError={saveError}
+ clearSaveError={clearSaveError}
+ isEditing={true}
+ isLegacy={isLegacy}
+ history={history as ScopedHistory}
+ />
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts
index f4d3426439562..3b1d5cf22452d 100644
--- a/x-pack/plugins/index_management/public/application/services/use_request.ts
+++ b/x-pack/plugins/index_management/public/application/services/use_request.ts
@@ -11,6 +11,7 @@ import {
UseRequestConfig,
sendRequest as _sendRequest,
useRequest as _useRequest,
+ Error,
} from '../../shared_imports';
import { httpService } from './http';
@@ -19,6 +20,6 @@ export const sendRequest = (config: SendRequestConfig): Promise(config: UseRequestConfig) => {
- return _useRequest(httpService.httpClient, config);
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(httpService.httpClient, config);
};
diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts
index eddac8e4b8a86..fa27b22e502fa 100644
--- a/x-pack/plugins/index_management/public/shared_imports.ts
+++ b/x-pack/plugins/index_management/public/shared_imports.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+export { APP_WRAPPER_CLASS } from '../../../../src/core/public';
+
export {
SendRequestConfig,
SendRequestResponse,
@@ -16,6 +18,10 @@ export {
extractQueryParams,
GlobalFlyout,
attemptToURIDecode,
+ PageLoading,
+ PageError,
+ Error,
+ SectionLoading,
} from '../../../../src/plugins/es_ui_shared/public';
export {
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
index bd000186d91c4..231a2764d2710 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
@@ -17,28 +17,40 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
-export function registerGetAllRoute({ router }: RouteDependencies) {
+export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) {
router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
- const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
- const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate');
- const { index_templates: templatesEs } = await callAsCurrentUser(
- 'dataManagement.getComposableIndexTemplates'
- );
-
- const legacyTemplates = deserializeLegacyTemplateList(
- legacyTemplatesEs,
- cloudManagedTemplatePrefix
- );
- const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
-
- const body = {
- templates,
- legacyTemplates,
- };
-
- return res.ok({ body });
+ try {
+ const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
+
+ const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate');
+ const { index_templates: templatesEs } = await callAsCurrentUser(
+ 'dataManagement.getComposableIndexTemplates'
+ );
+
+ const legacyTemplates = deserializeLegacyTemplateList(
+ legacyTemplatesEs,
+ cloudManagedTemplatePrefix
+ );
+ const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
+
+ const body = {
+ templates,
+ legacyTemplates,
+ };
+
+ return res.ok({ body });
+ } catch (error) {
+ if (isEsError(error)) {
+ return res.customError({
+ statusCode: error.statusCode,
+ body: error,
+ });
+ }
+ // Case: default
+ throw error;
+ }
});
}
diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts
index 8948a3e8d56be..d120f60ef8a2d 100644
--- a/x-pack/plugins/ingest_pipelines/public/index.ts
+++ b/x-pack/plugins/ingest_pipelines/public/index.ts
@@ -10,10 +10,3 @@ import { IngestPipelinesPlugin } from './plugin';
export function plugin() {
return new IngestPipelinesPlugin();
}
-
-export {
- INGEST_PIPELINES_APP_ULR_GENERATOR,
- IngestPipelinesUrlGenerator,
- IngestPipelinesUrlGeneratorState,
- INGEST_PIPELINES_PAGES,
-} from './url_generator';
diff --git a/x-pack/plugins/ingest_pipelines/public/locator.test.ts b/x-pack/plugins/ingest_pipelines/public/locator.test.ts
new file mode 100644
index 0000000000000..0b1246b2bed59
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/locator.test.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ManagementAppLocatorDefinition } from 'src/plugins/management/common/locator';
+import { IngestPipelinesLocatorDefinition, INGEST_PIPELINES_PAGES } from './locator';
+
+describe('Ingest pipeline locator', () => {
+ const setup = () => {
+ const managementDefinition = new ManagementAppLocatorDefinition();
+ const definition = new IngestPipelinesLocatorDefinition({
+ managementAppLocator: {
+ getLocation: (params) => managementDefinition.getLocation(params),
+ getUrl: async () => {
+ throw new Error('not implemented');
+ },
+ navigate: async () => {
+ throw new Error('not implemented');
+ },
+ useUrl: () => '',
+ },
+ });
+ return { definition };
+ };
+
+ describe('Pipelines List', () => {
+ it('generates relative url for list without pipelineId', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.LIST,
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines',
+ });
+ });
+
+ it('generates relative url for list with a pipelineId', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.LIST,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/?pipeline=pipeline_name',
+ });
+ });
+ });
+
+ describe('Pipeline Edit', () => {
+ it('generates relative url for pipeline edit', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.EDIT,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/edit/pipeline_name',
+ });
+ });
+ });
+
+ describe('Pipeline Clone', () => {
+ it('generates relative url for pipeline clone', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.CLONE,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/create/pipeline_name',
+ });
+ });
+ });
+
+ describe('Pipeline Create', () => {
+ it('generates relative url for pipeline create', async () => {
+ const { definition } = setup();
+ const location = await definition.getLocation({
+ page: INGEST_PIPELINES_PAGES.CREATE,
+ pipelineId: 'pipeline_name',
+ });
+
+ expect(location).toMatchObject({
+ app: 'management',
+ path: '/ingest/ingest_pipelines/create',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/public/locator.ts b/x-pack/plugins/ingest_pipelines/public/locator.ts
new file mode 100644
index 0000000000000..d819011f14f47
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/locator.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SerializableState } from 'src/plugins/kibana_utils/common';
+import { ManagementAppLocator } from 'src/plugins/management/common';
+import {
+ LocatorPublic,
+ LocatorDefinition,
+ KibanaLocation,
+} from '../../../../src/plugins/share/public';
+import {
+ getClonePath,
+ getCreatePath,
+ getEditPath,
+ getListPath,
+} from './application/services/navigation';
+import { PLUGIN_ID } from '../common/constants';
+
+export enum INGEST_PIPELINES_PAGES {
+ LIST = 'pipelines_list',
+ EDIT = 'pipeline_edit',
+ CREATE = 'pipeline_create',
+ CLONE = 'pipeline_clone',
+}
+
+interface IngestPipelinesBaseParams extends SerializableState {
+ pipelineId: string;
+}
+export interface IngestPipelinesListParams extends Partial {
+ page: INGEST_PIPELINES_PAGES.LIST;
+}
+
+export interface IngestPipelinesEditParams extends IngestPipelinesBaseParams {
+ page: INGEST_PIPELINES_PAGES.EDIT;
+}
+
+export interface IngestPipelinesCloneParams extends IngestPipelinesBaseParams {
+ page: INGEST_PIPELINES_PAGES.CLONE;
+}
+
+export interface IngestPipelinesCreateParams extends IngestPipelinesBaseParams {
+ page: INGEST_PIPELINES_PAGES.CREATE;
+}
+
+export type IngestPipelinesParams =
+ | IngestPipelinesListParams
+ | IngestPipelinesEditParams
+ | IngestPipelinesCloneParams
+ | IngestPipelinesCreateParams;
+
+export type IngestPipelinesLocator = LocatorPublic;
+
+export const INGEST_PIPELINES_APP_LOCATOR = 'INGEST_PIPELINES_APP_LOCATOR';
+
+export interface IngestPipelinesLocatorDependencies {
+ managementAppLocator: ManagementAppLocator;
+}
+
+export class IngestPipelinesLocatorDefinition implements LocatorDefinition {
+ public readonly id = INGEST_PIPELINES_APP_LOCATOR;
+
+ constructor(protected readonly deps: IngestPipelinesLocatorDependencies) {}
+
+ public readonly getLocation = async (params: IngestPipelinesParams): Promise => {
+ const location = await this.deps.managementAppLocator.getLocation({
+ sectionId: 'ingest',
+ appId: PLUGIN_ID,
+ });
+
+ let path: string = '';
+
+ switch (params.page) {
+ case INGEST_PIPELINES_PAGES.EDIT:
+ path = getEditPath({
+ pipelineName: params.pipelineId,
+ });
+ break;
+ case INGEST_PIPELINES_PAGES.CREATE:
+ path = getCreatePath();
+ break;
+ case INGEST_PIPELINES_PAGES.LIST:
+ path = getListPath({
+ inspectedPipelineName: params.pipelineId,
+ });
+ break;
+ case INGEST_PIPELINES_PAGES.CLONE:
+ path = getClonePath({
+ clonedPipelineName: params.pipelineId,
+ });
+ break;
+ }
+
+ return {
+ ...location,
+ path: path === '/' ? location.path : location.path + path,
+ };
+ };
+}
diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts
index 4a138a12d6819..b4eb33162a1f4 100644
--- a/x-pack/plugins/ingest_pipelines/public/plugin.ts
+++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts
@@ -11,7 +11,7 @@ import { CoreSetup, Plugin } from 'src/core/public';
import { PLUGIN_ID } from '../common/constants';
import { uiMetricService, apiService } from './application/services';
import { SetupDependencies, StartDependencies } from './types';
-import { registerUrlGenerator } from './url_generator';
+import { IngestPipelinesLocatorDefinition } from './locator';
export class IngestPipelinesPlugin
implements Plugin {
@@ -50,7 +50,11 @@ export class IngestPipelinesPlugin
},
});
- registerUrlGenerator(coreSetup, management, share);
+ share.url.locators.create(
+ new IngestPipelinesLocatorDefinition({
+ managementAppLocator: management.locator,
+ })
+ );
}
public start() {}
diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts
deleted file mode 100644
index dc45f9bc39088..0000000000000
--- a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { IngestPipelinesUrlGenerator, INGEST_PIPELINES_PAGES } from './url_generator';
-
-describe('IngestPipelinesUrlGenerator', () => {
- const getAppBasePath = (absolute: boolean = false) => {
- if (absolute) {
- return Promise.resolve('http://localhost/app/test_app');
- }
- return Promise.resolve('/app/test_app');
- };
- const urlGenerator = new IngestPipelinesUrlGenerator(getAppBasePath);
-
- describe('Pipelines List', () => {
- it('generates relative url for list without pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- });
- expect(url).toBe('/app/test_app/');
- });
-
- it('generates absolute url for list without pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/');
- });
- it('generates relative url for list with a pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/?pipeline=pipeline_name');
- });
-
- it('generates absolute url for list with a pipelineId', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.LIST,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/?pipeline=pipeline_name');
- });
- });
-
- describe('Pipeline Edit', () => {
- it('generates relative url for pipeline edit', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.EDIT,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/edit/pipeline_name');
- });
-
- it('generates absolute url for pipeline edit', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.EDIT,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/edit/pipeline_name');
- });
- });
-
- describe('Pipeline Clone', () => {
- it('generates relative url for pipeline clone', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CLONE,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/create/pipeline_name');
- });
-
- it('generates absolute url for pipeline clone', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CLONE,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/create/pipeline_name');
- });
- });
-
- describe('Pipeline Create', () => {
- it('generates relative url for pipeline create', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CREATE,
- pipelineId: 'pipeline_name',
- });
- expect(url).toBe('/app/test_app/create');
- });
-
- it('generates absolute url for pipeline create', async () => {
- const url = await urlGenerator.createUrl({
- page: INGEST_PIPELINES_PAGES.CREATE,
- pipelineId: 'pipeline_name',
- absolute: true,
- });
- expect(url).toBe('http://localhost/app/test_app/create');
- });
- });
-});
diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.ts
deleted file mode 100644
index d9a77addcd5fd..0000000000000
--- a/x-pack/plugins/ingest_pipelines/public/url_generator.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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { CoreSetup } from 'src/core/public';
-import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public';
-import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public';
-import {
- getClonePath,
- getCreatePath,
- getEditPath,
- getListPath,
-} from './application/services/navigation';
-import { SetupDependencies } from './types';
-import { PLUGIN_ID } from '../common/constants';
-
-export const INGEST_PIPELINES_APP_ULR_GENERATOR = 'INGEST_PIPELINES_APP_URL_GENERATOR';
-
-export enum INGEST_PIPELINES_PAGES {
- LIST = 'pipelines_list',
- EDIT = 'pipeline_edit',
- CREATE = 'pipeline_create',
- CLONE = 'pipeline_clone',
-}
-
-interface UrlGeneratorState {
- pipelineId: string;
- absolute?: boolean;
-}
-export interface PipelinesListUrlGeneratorState extends Partial {
- page: INGEST_PIPELINES_PAGES.LIST;
-}
-
-export interface PipelineEditUrlGeneratorState extends UrlGeneratorState {
- page: INGEST_PIPELINES_PAGES.EDIT;
-}
-
-export interface PipelineCloneUrlGeneratorState extends UrlGeneratorState {
- page: INGEST_PIPELINES_PAGES.CLONE;
-}
-
-export interface PipelineCreateUrlGeneratorState extends UrlGeneratorState {
- page: INGEST_PIPELINES_PAGES.CREATE;
-}
-
-export type IngestPipelinesUrlGeneratorState =
- | PipelinesListUrlGeneratorState
- | PipelineEditUrlGeneratorState
- | PipelineCloneUrlGeneratorState
- | PipelineCreateUrlGeneratorState;
-
-export class IngestPipelinesUrlGenerator
- implements UrlGeneratorsDefinition {
- constructor(private readonly getAppBasePath: (absolute: boolean) => Promise) {}
-
- public readonly id = INGEST_PIPELINES_APP_ULR_GENERATOR;
-
- public readonly createUrl = async (state: IngestPipelinesUrlGeneratorState): Promise => {
- switch (state.page) {
- case INGEST_PIPELINES_PAGES.EDIT: {
- return `${await this.getAppBasePath(!!state.absolute)}${getEditPath({
- pipelineName: state.pipelineId,
- })}`;
- }
- case INGEST_PIPELINES_PAGES.CREATE: {
- return `${await this.getAppBasePath(!!state.absolute)}${getCreatePath()}`;
- }
- case INGEST_PIPELINES_PAGES.LIST: {
- return `${await this.getAppBasePath(!!state.absolute)}${getListPath({
- inspectedPipelineName: state.pipelineId,
- })}`;
- }
- case INGEST_PIPELINES_PAGES.CLONE: {
- return `${await this.getAppBasePath(!!state.absolute)}${getClonePath({
- clonedPipelineName: state.pipelineId,
- })}`;
- }
- }
- };
-}
-
-export const registerUrlGenerator = (
- coreSetup: CoreSetup,
- management: SetupDependencies['management'],
- share: SetupDependencies['share']
-) => {
- const getAppBasePath = async (absolute = false) => {
- const [coreStart] = await coreSetup.getStartServices();
- return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, {
- path: management.sections.section.ingest.getApp(PLUGIN_ID)!.basePath,
- absolute: !!absolute,
- });
- };
-
- share.urlGenerators.registerUrlGenerator(new IngestPipelinesUrlGenerator(getAppBasePath));
-};
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
index 972ef99d7d7f6..4a34bd030429e 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
@@ -6,7 +6,8 @@
*/
import React from 'react';
-import { shallow } from 'enzyme';
+import { ReactWrapper, shallow } from 'enzyme';
+import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test/jest';
import { EuiDataGrid } from '@elastic/eui';
import { IAggType, IFieldFormat } from 'src/plugins/data/public';
@@ -83,6 +84,13 @@ function copyData(data: LensMultiTable): LensMultiTable {
return JSON.parse(JSON.stringify(data));
}
+async function waitForWrapperUpdate(wrapper: ReactWrapper) {
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 0));
+ });
+ wrapper.update();
+}
+
describe('DatatableComponent', () => {
let onDispatchEvent: jest.Mock;
@@ -149,7 +157,7 @@ describe('DatatableComponent', () => {
).toMatchSnapshot();
});
- test('it invokes executeTriggerActions with correct context on click on top value', () => {
+ test('it invokes executeTriggerActions with correct context on click on top value', async () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
@@ -173,6 +181,8 @@ describe('DatatableComponent', () => {
wrapper.find('[data-test-subj="dataGridRowCell"]').first().simulate('focus');
+ await waitForWrapperUpdate(wrapper);
+
wrapper.find('[data-test-subj="lensDatatableFilterOut"]').first().simulate('click');
expect(onDispatchEvent).toHaveBeenCalledWith({
@@ -192,7 +202,7 @@ describe('DatatableComponent', () => {
});
});
- test('it invokes executeTriggerActions with correct context on click on timefield', () => {
+ test('it invokes executeTriggerActions with correct context on click on timefield', async () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
@@ -216,6 +226,8 @@ describe('DatatableComponent', () => {
wrapper.find('[data-test-subj="dataGridRowCell"]').at(1).simulate('focus');
+ await waitForWrapperUpdate(wrapper);
+
wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click');
expect(onDispatchEvent).toHaveBeenCalledWith({
@@ -235,7 +247,7 @@ describe('DatatableComponent', () => {
});
});
- test('it invokes executeTriggerActions with correct context on click on timefield from range', () => {
+ test('it invokes executeTriggerActions with correct context on click on timefield from range', async () => {
const data: LensMultiTable = {
type: 'lens_multitable',
tables: {
@@ -298,6 +310,8 @@ describe('DatatableComponent', () => {
wrapper.find('[data-test-subj="dataGridRowCell"]').at(0).simulate('focus');
+ await waitForWrapperUpdate(wrapper);
+
wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click');
expect(onDispatchEvent).toHaveBeenCalledWith({
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index 52488cb32ae83..0e2ba5ce8ad59 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -1370,6 +1370,57 @@ describe('editor_frame', () => {
})
);
});
+
+ it('should avoid completely to compute suggestion when in fullscreen mode', async () => {
+ const props = {
+ ...getDefaultProps(),
+ initialContext: {
+ indexPatternId: '1',
+ fieldName: 'test',
+ },
+ visualizationMap: {
+ testVis: mockVisualization,
+ },
+ datasourceMap: {
+ testDatasource: mockDatasource,
+ testDatasource2: mockDatasource2,
+ },
+
+ ExpressionRenderer: expressionRendererMock,
+ };
+
+ const { instance: el } = await mountWithProvider(
+ ,
+ props.plugins.data
+ );
+ instance = el;
+
+ expect(
+ instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement
+ ).not.toBeUndefined();
+
+ await act(async () => {
+ (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
+ type: 'TOGGLE_FULLSCREEN',
+ });
+ });
+
+ instance.update();
+
+ expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false);
+
+ await act(async () => {
+ (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
+ type: 'TOGGLE_FULLSCREEN',
+ });
+ });
+
+ instance.update();
+
+ expect(
+ instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement
+ ).not.toBeUndefined();
+ });
});
describe('passing state back to the caller', () => {
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 cc65bb126d2d9..bd96682f427fa 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
@@ -452,7 +452,8 @@ export function EditorFrame(props: EditorFrameProps) {
)
}
suggestionsPanel={
- allLoaded && (
+ allLoaded &&
+ !state.isFullscreenDatasource && (
{
const parsedValue = parseTimeShift(value);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
index 03b9d6c07709c..87116f71919b5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
@@ -7,11 +7,12 @@
import { i18n } from '@kbn/i18n';
import type { ExpressionFunctionAST } from '@kbn/interpreter/common';
+import memoizeOne from 'memoize-one';
import type { TimeScaleUnit } from '../../../time_scale';
import type { IndexPattern, IndexPatternLayer } from '../../../types';
import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils';
import type { ReferenceBasedIndexPatternColumn } from '../column_types';
-import { isColumnValidAsReference } from '../../layer_helpers';
+import { getManagedColumnsFrom, isColumnValidAsReference } from '../../layer_helpers';
import { operationDefinitionMap } from '..';
export const buildLabelFunction = (ofName: (name?: string) => string) => (
@@ -45,6 +46,23 @@ export function checkForDateHistogram(layer: IndexPatternLayer, name: string) {
];
}
+const getFullyManagedColumnIds = memoizeOne((layer: IndexPatternLayer) => {
+ const managedColumnIds = new Set();
+ Object.entries(layer.columns).forEach(([id, column]) => {
+ if (
+ 'references' in column &&
+ operationDefinitionMap[column.operationType].input === 'managedReference'
+ ) {
+ managedColumnIds.add(id);
+ const managedColumns = getManagedColumnsFrom(id, layer.columns);
+ managedColumns.map(([managedId]) => {
+ managedColumnIds.add(managedId);
+ });
+ }
+ });
+ return managedColumnIds;
+});
+
export function checkReferences(layer: IndexPatternLayer, columnId: string) {
const column = layer.columns[columnId] as ReferenceBasedIndexPatternColumn;
@@ -72,7 +90,8 @@ export function checkReferences(layer: IndexPatternLayer, columnId: string) {
column: referenceColumn,
});
- if (!isValid) {
+ // do not enforce column validity if current column is part of managed subtree
+ if (!isValid && !getFullyManagedColumnIds(layer).has(columnId)) {
errors.push(
i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', {
defaultMessage: 'Dimension "{dimensionLabel}" is configured incorrectly',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
index 654a93374703d..d1b0ec8876feb 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx
@@ -29,7 +29,7 @@ import { ParamEditorProps } from '../../index';
import { getManagedColumnsFrom } from '../../../layer_helpers';
import { ErrorWrapper, runASTValidation, tryToParse } from '../validation';
import {
- LensMathSuggestion,
+ LensMathSuggestions,
SUGGESTION_TYPE,
suggest,
getSuggestion,
@@ -329,7 +329,7 @@ export function FormulaEditor({
context: monaco.languages.CompletionContext
) => {
const innerText = model.getValue();
- let aSuggestions: { list: LensMathSuggestion[]; type: SUGGESTION_TYPE } = {
+ let aSuggestions: LensMathSuggestions = {
list: [],
type: SUGGESTION_TYPE.FIELD,
};
@@ -367,7 +367,13 @@ export function FormulaEditor({
return {
suggestions: aSuggestions.list.map((s) =>
- getSuggestion(s, aSuggestions.type, visibleOperationsMap, context.triggerCharacter)
+ getSuggestion(
+ s,
+ aSuggestions.type,
+ visibleOperationsMap,
+ context.triggerCharacter,
+ aSuggestions.range
+ )
),
};
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
index 9cd748f5759c9..c55f22dd682d0 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
@@ -18,6 +18,7 @@ import {
getHover,
suggest,
monacoPositionToOffset,
+ offsetToRowColumn,
getInfoAtZeroIndexedPosition,
} from './math_completion';
@@ -363,6 +364,36 @@ describe('math completion', () => {
});
});
+ describe('offsetToRowColumn', () => {
+ it('should work with single-line strings', () => {
+ const input = `0123456`;
+ expect(offsetToRowColumn(input, 5)).toEqual(
+ expect.objectContaining({
+ lineNumber: 1,
+ column: 6,
+ })
+ );
+ });
+
+ it('should work with multi-line strings accounting for newline characters', () => {
+ const input = `012
+456
+89')`;
+ expect(offsetToRowColumn(input, 0)).toEqual(
+ expect.objectContaining({
+ lineNumber: 1,
+ column: 1,
+ })
+ );
+ expect(offsetToRowColumn(input, 9)).toEqual(
+ expect.objectContaining({
+ lineNumber: 3,
+ column: 2,
+ })
+ );
+ });
+ });
+
describe('monacoPositionToOffset', () => {
it('should work with multi-line strings accounting for newline characters', () => {
const input = `012
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
index 815df943cdba3..28e762e7dff0f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts
@@ -13,6 +13,7 @@ import {
TinymathLocation,
TinymathAST,
TinymathFunction,
+ TinymathVariable,
TinymathNamedArgument,
} from '@kbn/tinymath';
import type {
@@ -21,7 +22,7 @@ import type {
} from '../../../../../../../../../src/plugins/data/public';
import { IndexPattern } from '../../../../types';
import { memoizedGetAvailableOperationsByMetadata } from '../../../operations';
-import { tinymathFunctions, groupArgsByType } from '../util';
+import { tinymathFunctions, groupArgsByType, unquotedStringRegex } from '../util';
import type { GenericOperationDefinition } from '../..';
import { getFunctionSignatureLabel, getHelpTextContent } from './formula_help';
import { hasFunctionFieldArgument } from '../validation';
@@ -47,6 +48,7 @@ export type LensMathSuggestion =
export interface LensMathSuggestions {
list: LensMathSuggestion[];
type: SUGGESTION_TYPE;
+ range?: monaco.IRange;
}
function inLocation(cursorPosition: number, location: TinymathLocation) {
@@ -92,7 +94,7 @@ export function offsetToRowColumn(expression: string, offset: number): monaco.Po
let lineNumber = 1;
for (const line of lines) {
if (line.length >= remainingChars) {
- return new monaco.Position(lineNumber, remainingChars);
+ return new monaco.Position(lineNumber, remainingChars + 1);
}
remainingChars -= line.length + 1;
lineNumber++;
@@ -128,7 +130,7 @@ export async function suggest({
operationDefinitionMap: Record;
data: DataPublicPluginStart;
dateHistogramInterval?: number;
-}): Promise<{ list: LensMathSuggestion[]; type: SUGGESTION_TYPE }> {
+}): Promise {
const text =
expression.substr(0, zeroIndexedOffset) + MARKER + expression.substr(zeroIndexedOffset);
try {
@@ -154,6 +156,7 @@ export async function suggest({
return getArgumentSuggestions(
tokenInfo.parent,
tokenInfo.parent.args.findIndex((a) => a === tokenAst),
+ text,
indexPattern,
operationDefinitionMap
);
@@ -210,6 +213,7 @@ function getFunctionSuggestions(
function getArgumentSuggestions(
ast: TinymathFunction,
position: number,
+ expression: string,
indexPattern: IndexPattern,
operationDefinitionMap: Record
) {
@@ -280,7 +284,16 @@ function getArgumentSuggestions(
.filter((op) => op.operationType === operation.type)
.map((op) => ('field' in op ? op.field : undefined))
.filter((field) => field);
- return { list: fields as string[], type: SUGGESTION_TYPE.FIELD };
+ const fieldArg = ast.args[0];
+ const location = typeof fieldArg !== 'string' && (fieldArg as TinymathVariable).location;
+ let range: monaco.IRange | undefined;
+ if (location) {
+ const start = offsetToRowColumn(expression, location.min);
+ // This accounts for any characters that the user has already typed
+ const end = offsetToRowColumn(expression, location.max - MARKER.length);
+ range = monaco.Range.fromPositions(start, end);
+ }
+ return { list: fields as string[], type: SUGGESTION_TYPE.FIELD, range };
} else {
return { list: [], type: SUGGESTION_TYPE.FIELD };
}
@@ -375,7 +388,8 @@ export function getSuggestion(
suggestion: LensMathSuggestion,
type: SUGGESTION_TYPE,
operationDefinitionMap: Record,
- triggerChar: string | undefined
+ triggerChar: string | undefined,
+ range?: monaco.IRange
): monaco.languages.CompletionItem {
let kind: monaco.languages.CompletionItemKind = monaco.languages.CompletionItemKind.Method;
let label: string =
@@ -397,6 +411,10 @@ export function getSuggestion(
break;
case SUGGESTION_TYPE.FIELD:
kind = monaco.languages.CompletionItemKind.Value;
+ // Look for unsafe characters
+ if (unquotedStringRegex.test(label)) {
+ insertText = `'${label.replaceAll(`'`, "\\'")}'`;
+ }
break;
case SUGGESTION_TYPE.FUNCTIONS:
insertText = `${label}($0)`;
@@ -450,7 +468,7 @@ export function getSuggestion(
command,
additionalTextEdits: [],
// @ts-expect-error Monaco says this type is required, but provides a default value
- range: undefined,
+ range,
sortText,
filterText,
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
index e6aa29ea4d763..279e76b839548 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
@@ -413,13 +413,13 @@ describe('formula', () => {
).newLayer
).toEqual({
...layer,
- columnOrder: ['col1X0', 'col1X1', 'col1'],
+ columnOrder: ['col1X0', 'col1'],
columns: {
...layer.columns,
col1: {
...currentColumn,
label: 'average(bytes)',
- references: ['col1X1'],
+ references: ['col1X0'],
params: {
...currentColumn.params,
formula: 'average(bytes)',
@@ -436,18 +436,6 @@ describe('formula', () => {
sourceField: 'bytes',
timeScale: false,
},
- col1X1: {
- customLabel: true,
- dataType: 'number',
- isBucketed: false,
- label: 'Part of average(bytes)',
- operationType: 'math',
- params: {
- tinymathAst: 'col1X0',
- },
- references: ['col1X0'],
- scale: 'ratio',
- },
},
});
});
@@ -568,8 +556,8 @@ describe('formula', () => {
).locations
).toEqual({
col1X0: { min: 15, max: 29 },
- col1X2: { min: 0, max: 41 },
- col1X3: { min: 42, max: 50 },
+ col1X1: { min: 0, max: 41 },
+ col1X2: { min: 42, max: 50 },
});
});
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
index a5c19c537acee..589f547434b91 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts
@@ -13,6 +13,7 @@ import {
} from '../index';
import { ReferenceBasedIndexPatternColumn } from '../column_types';
import { IndexPatternLayer } from '../../../types';
+import { unquotedStringRegex } from './util';
// Just handle two levels for now
type OperationParams = Record>;
@@ -25,6 +26,9 @@ export function getSafeFieldName({
if (!fieldName || operationType === 'count') {
return '';
}
+ if (unquotedStringRegex.test(fieldName)) {
+ return `'${fieldName.replaceAll(`'`, "\\'")}'`;
+ }
return fieldName;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
index 8b726d06f4602..cb1d0dc143efc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
@@ -123,17 +123,20 @@ function extractColumns(
if (nodeOperation.input === 'fullReference') {
const [referencedOp] = functions;
const consumedParam = parseNode(referencedOp);
+ const hasActualMathContent = typeof consumedParam !== 'string';
- const subNodeVariables = consumedParam ? findVariables(consumedParam) : [];
- const mathColumn = mathOperation.buildColumn({
- layer,
- indexPattern,
- });
- mathColumn.references = subNodeVariables.map(({ value }) => value);
- mathColumn.params.tinymathAst = consumedParam!;
- columns.push({ column: mathColumn });
- mathColumn.customLabel = true;
- mathColumn.label = label;
+ if (hasActualMathContent) {
+ const subNodeVariables = consumedParam ? findVariables(consumedParam) : [];
+ const mathColumn = mathOperation.buildColumn({
+ layer,
+ indexPattern,
+ });
+ mathColumn.references = subNodeVariables.map(({ value }) => value);
+ mathColumn.params.tinymathAst = consumedParam!;
+ columns.push({ column: mathColumn });
+ mathColumn.customLabel = true;
+ mathColumn.label = label;
+ }
const mappedParams = getOperationParams(nodeOperation, namedArguments || []);
const newCol = (nodeOperation as OperationDefinition<
@@ -143,7 +146,11 @@ function extractColumns(
{
layer,
indexPattern,
- referenceIds: [getManagedId(idPrefix, columns.length - 1)],
+ referenceIds: [
+ hasActualMathContent
+ ? getManagedId(idPrefix, columns.length - 1)
+ : (consumedParam as string),
+ ],
},
mappedParams
);
@@ -160,16 +167,19 @@ function extractColumns(
if (root === undefined) {
return [];
}
- const variables = findVariables(root);
- const mathColumn = mathOperation.buildColumn({
- layer,
- indexPattern,
- });
- mathColumn.references = variables.map(({ value }) => value);
- mathColumn.params.tinymathAst = root!;
- mathColumn.customLabel = true;
- mathColumn.label = label;
- columns.push({ column: mathColumn });
+ const topLevelMath = typeof root !== 'string';
+ if (topLevelMath) {
+ const variables = findVariables(root);
+ const mathColumn = mathOperation.buildColumn({
+ layer,
+ indexPattern,
+ });
+ mathColumn.references = variables.map(({ value }) => value);
+ mathColumn.params.tinymathAst = root!;
+ mathColumn.customLabel = true;
+ mathColumn.label = label;
+ columns.push({ column: mathColumn });
+ }
return columns;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
index d29682eafa329..9806cdaad637e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts
@@ -16,6 +16,8 @@ import type {
import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index';
import type { GroupedNodes } from './types';
+export const unquotedStringRegex = /[^0-9A-Za-z._@\[\]/]/;
+
export function groupArgsByType(args: TinymathAST[]) {
const { namedArgument, variable, function: functions } = groupBy(
args,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx
index 7551b88039182..a458a1edcfa16 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx
@@ -424,25 +424,9 @@ export const termsOperation: OperationDefinition
- {i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
- defaultMessage: 'Rank direction',
- })}{' '}
-
- >
- }
+ label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
+ defaultMessage: 'Rank direction',
+ })}
display="columnCompressed"
fullWidth
>
@@ -513,7 +497,10 @@ export const termsOperation: OperationDefinition
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
index 3b557461546ca..f326f3e3ed5f6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
@@ -60,7 +60,7 @@ describe('terms', () => {
size: 3,
orderDirection: 'asc',
},
- sourceField: 'category',
+ sourceField: 'source',
},
col2: {
label: 'Count',
@@ -88,7 +88,7 @@ describe('terms', () => {
expect.objectContaining({
arguments: expect.objectContaining({
orderBy: ['_key'],
- field: ['category'],
+ field: ['source'],
size: [3],
otherBucket: [true],
}),
@@ -770,6 +770,34 @@ describe('terms', () => {
expect(select.prop('disabled')).toEqual(false);
});
+ it('should disable missing bucket setting if field is not a string', () => {
+ const updateLayerSpy = jest.fn();
+ const instance = shallow(
+
+ );
+
+ const select = instance
+ .find('[data-test-subj="indexPattern-terms-missing-bucket"]')
+ .find(EuiSwitch);
+
+ expect(select.prop('disabled')).toEqual(true);
+ });
+
it('should update state when clicking other bucket toggle', () => {
const updateLayerSpy = jest.fn();
const instance = shallow(
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index 387a61ff79264..7de1318cbac61 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -25,6 +25,7 @@ import { documentField } from '../document_field';
import { getFieldByNameFactory } from '../pure_helpers';
import { generateId } from '../../id_generator';
import { createMockedFullReference, createMockedManagedReference } from './mocks';
+import { TinymathAST } from 'packages/kbn-tinymath';
jest.mock('../operations');
jest.mock('../../id_generator');
@@ -105,28 +106,34 @@ describe('state_helpers', () => {
const source = {
dataType: 'number' as const,
isBucketed: false,
- label: 'moving_average(sum(bytes), window=5)',
+ label: '5 + moving_average(sum(bytes), window=5)',
operationType: 'formula' as const,
params: {
- formula: 'moving_average(sum(bytes), window=5)',
+ formula: '5 + moving_average(sum(bytes), window=5)',
isFormulaBroken: false,
},
- references: ['formulaX1'],
+ references: ['formulaX2'],
};
const math = {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- label: 'Part of moving_average(sum(bytes), window=5)',
operationType: 'math' as const,
- params: { tinymathAst: 'formulaX2' },
- references: ['formulaX2'],
+ label: 'Part of 5 + moving_average(sum(bytes), window=5)',
+ references: ['formulaX1'],
+ params: {
+ tinymathAst: {
+ type: 'function',
+ name: 'add',
+ args: [5, 'formulaX1'],
+ } as TinymathAST,
+ },
};
const sum = {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- label: 'Part of moving_average(sum(bytes), window=5)',
+ label: 'Part of 5 + moving_average(sum(bytes), window=5)',
operationType: 'sum' as const,
scale: 'ratio' as const,
sourceField: 'bytes',
@@ -135,7 +142,7 @@ describe('state_helpers', () => {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- label: 'Part of moving_average(sum(bytes), window=5)',
+ label: 'Part of 5 + moving_average(sum(bytes), window=5)',
operationType: 'moving_average' as const,
params: { window: 5 },
references: ['formulaX0'],
@@ -148,14 +155,8 @@ describe('state_helpers', () => {
columns: {
source,
formulaX0: sum,
- formulaX1: math,
- formulaX2: movingAvg,
- formulaX3: {
- ...math,
- label: 'Part of moving_average(sum(bytes), window=5)',
- references: ['formulaX2'],
- params: { tinymathAst: 'formulaX2' },
- },
+ formulaX1: movingAvg,
+ formulaX2: math,
},
},
targetId: 'copy',
@@ -171,40 +172,34 @@ describe('state_helpers', () => {
'formulaX0',
'formulaX1',
'formulaX2',
- 'formulaX3',
'copyX0',
'copyX1',
'copyX2',
- 'copyX3',
'copy',
],
columns: {
source,
formulaX0: sum,
- formulaX1: math,
- formulaX2: movingAvg,
- formulaX3: {
- ...math,
- references: ['formulaX2'],
- params: { tinymathAst: 'formulaX2' },
- },
- copy: expect.objectContaining({ ...source, references: ['copyX3'] }),
+ formulaX1: movingAvg,
+ formulaX2: math,
+ copy: expect.objectContaining({ ...source, references: ['copyX2'] }),
copyX0: expect.objectContaining({
...sum,
}),
copyX1: expect.objectContaining({
- ...math,
+ ...movingAvg,
references: ['copyX0'],
- params: { tinymathAst: 'copyX0' },
}),
copyX2: expect.objectContaining({
- ...movingAvg,
- references: ['copyX1'],
- }),
- copyX3: expect.objectContaining({
...math,
- references: ['copyX2'],
- params: { tinymathAst: 'copyX2' },
+ references: ['copyX1'],
+ params: {
+ tinymathAst: expect.objectContaining({
+ type: 'function',
+ name: 'add',
+ args: [5, 'copyX1'],
+ } as TinymathAST),
+ },
}),
},
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx
index 14ba6b9189e6b..a1bc643c3bd93 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx
@@ -23,67 +23,67 @@ import { FramePublicAPI } from '../types';
export const timeShiftOptions = [
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', {
- defaultMessage: '1 hour (1h)',
+ defaultMessage: '1 hour ago (1h)',
}),
value: '1h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.3hours', {
- defaultMessage: '3 hours (3h)',
+ defaultMessage: '3 hours ago (3h)',
}),
value: '3h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.6hours', {
- defaultMessage: '6 hours (6h)',
+ defaultMessage: '6 hours ago (6h)',
}),
value: '6h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.12hours', {
- defaultMessage: '12 hours (12h)',
+ defaultMessage: '12 hours ago (12h)',
}),
value: '12h',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.day', {
- defaultMessage: '1 day (1d)',
+ defaultMessage: '1 day ago (1d)',
}),
value: '1d',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.week', {
- defaultMessage: '1 week (1w)',
+ defaultMessage: '1 week ago (1w)',
}),
value: '1w',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.month', {
- defaultMessage: '1 month (1M)',
+ defaultMessage: '1 month ago (1M)',
}),
value: '1M',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.3months', {
- defaultMessage: '3 months (3M)',
+ defaultMessage: '3 months ago (3M)',
}),
value: '3M',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.6months', {
- defaultMessage: '6 months (6M)',
+ defaultMessage: '6 months ago (6M)',
}),
value: '6M',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.year', {
- defaultMessage: '1 year (1y)',
+ defaultMessage: '1 year ago (1y)',
}),
value: '1y',
},
{
label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', {
- defaultMessage: 'Previous',
+ defaultMessage: 'Previous time range',
}),
value: 'previous',
},
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
index 29ec3ddbfdc02..45e7055f4db2b 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
@@ -1417,19 +1417,29 @@ exports[`UploadLicense should display an error when ES says license is expired 1
-
-
+
- The supplied license has expired.
-
-
+
+
+ The supplied license has expired.
+
+
+
+
@@ -2149,19 +2159,29 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
-
-
+
- The supplied license is not valid for this product.
-
-
+
+
+ The supplied license is not valid for this product.
+
+
+
+
@@ -2881,19 +2901,29 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
-
-
+
- Error encountered uploading license: Check your license file.
-
-
+
+
+ Error encountered uploading license: Check your license file.
+
+
+
+
@@ -3613,19 +3643,29 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
-
-
+
- Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled
-
-
+
+
+ Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled
+
+
+
+
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
index bdcb4224eed9c..4987de321c556 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
@@ -48,6 +48,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -83,6 +84,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -122,6 +124,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: true,
})
);
@@ -132,7 +135,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
+ '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -157,6 +160,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -167,7 +171,79 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ http: mockKibanaHttpService,
+ namespaceTypes: 'single,agnostic',
+ pagination: { page: 1, perPage: 20 },
+ signal: new AbortController().signal,
+ });
+ });
+ });
+
+ test('fetches event filters lists if "showEventFilters" is true', async () => {
+ const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
+
+ await act(async () => {
+ const { waitForNextUpdate } = renderHook(() =>
+ useExceptionLists({
+ errorMessage: 'Uh oh',
+ filterOptions: {},
+ http: mockKibanaHttpService,
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
+ pagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ showEventFilters: true,
+ showTrustedApps: false,
+ })
+ );
+ // NOTE: First `waitForNextUpdate` is initialization
+ // Second call applies the params
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
+ filters:
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ http: mockKibanaHttpService,
+ namespaceTypes: 'single,agnostic',
+ pagination: { page: 1, perPage: 20 },
+ signal: new AbortController().signal,
+ });
+ });
+ });
+
+ test('does not fetch event filters lists if "showEventFilters" is false', async () => {
+ const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
+
+ await act(async () => {
+ const { waitForNextUpdate } = renderHook(() =>
+ useExceptionLists({
+ errorMessage: 'Uh oh',
+ filterOptions: {},
+ http: mockKibanaHttpService,
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
+ pagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ showEventFilters: false,
+ showTrustedApps: false,
+ })
+ );
+ // NOTE: First `waitForNextUpdate` is initialization
+ // Second call applies the params
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
+ filters:
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -195,6 +271,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -205,7 +282,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)',
+ '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -228,6 +305,7 @@ describe('useExceptionLists', () => {
namespaceTypes,
notifications,
pagination,
+ showEventFilters,
showTrustedApps,
}) =>
useExceptionLists({
@@ -237,6 +315,7 @@ describe('useExceptionLists', () => {
namespaceTypes,
notifications,
pagination,
+ showEventFilters,
showTrustedApps,
}),
{
@@ -251,6 +330,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
},
}
@@ -271,6 +351,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
});
// NOTE: Only need one call here because hook already initilaized
@@ -298,6 +379,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
@@ -336,6 +418,7 @@ describe('useExceptionLists', () => {
perPage: 20,
total: 0,
},
+ showEventFilters: false,
showTrustedApps: false,
})
);
diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts
index 37a8e8063c4ed..fa065e701184e 100644
--- a/x-pack/plugins/maps/common/constants.ts
+++ b/x-pack/plugins/maps/common/constants.ts
@@ -58,15 +58,14 @@ export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__';
export const MVT_TOKEN_PARAM_NAME = 'token';
-const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`;
export function getNewMapPath() {
- return MAP_BASE_URL;
+ return `/${MAPS_APP_PATH}/${MAP_PATH}`;
}
-export function getExistingMapPath(id: string) {
- return `${MAP_BASE_URL}/${id}`;
+export function getFullPath(id: string | undefined) {
+ return `/${MAPS_APP_PATH}${getEditPath(id)}`;
}
-export function getEditPath(id: string) {
- return `/${MAP_PATH}/${id}`;
+export function getEditPath(id: string | undefined) {
+ return id ? `/${MAP_PATH}/${id}` : `/${MAP_PATH}`;
}
export enum LAYER_TYPE {
diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
index 07de57d0ac832..d1690ddfff43d 100644
--- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
@@ -66,6 +66,7 @@ export type VectorSourceRequestMeta = MapFilters & {
applyGlobalTime: boolean;
fieldNames: string[];
geogridPrecision?: number;
+ timesiceMaskField?: string;
sourceQuery?: MapQuery;
sourceMeta: VectorSourceSyncMeta;
};
@@ -84,6 +85,9 @@ export type VectorStyleRequestMeta = MapFilters & {
export type ESSearchSourceResponseMeta = {
areResultsTrimmed?: boolean;
resultsCount?: number;
+ // results time extent, either Kibana time range or timeslider time slice
+ timeExtent?: Timeslice;
+ isTimeExtentForTimeslice?: boolean;
// top hits meta
areEntitiesTrimmed?: boolean;
diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
index 6dd454137be7d..9bfa74825c338 100644
--- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
+++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
@@ -22,7 +22,6 @@ import {
LAYER_STYLE_TYPE,
FIELD_ORIGIN,
} from '../../../../common/constants';
-import { isTotalHitsGreaterThan, TotalHits } from '../../../../common/elasticsearch_util';
import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source';
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
import { IESSource } from '../../sources/es_source';
@@ -35,6 +34,7 @@ import {
DynamicStylePropertyOptions,
StylePropertyOptions,
LayerDescriptor,
+ Timeslice,
VectorLayerDescriptor,
VectorSourceRequestMeta,
VectorStylePropertiesDescriptor,
@@ -46,10 +46,6 @@ import { isSearchSourceAbortError } from '../../sources/es_source/es_source';
const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID';
-interface CountData {
- isSyncClustered: boolean;
-}
-
function getAggType(
dynamicProperty: IDynamicStyleProperty
): AGG_TYPE.AVG | AGG_TYPE.TERMS {
@@ -216,7 +212,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
let isClustered = false;
const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID);
if (countDataRequest) {
- const requestData = countDataRequest.getData() as CountData;
+ const requestData = countDataRequest.getData() as { isSyncClustered: boolean };
if (requestData && requestData.isSyncClustered) {
isClustered = true;
}
@@ -294,7 +290,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
async syncData(syncContext: DataRequestContext) {
const dataRequestId = ACTIVE_COUNT_DATA_ID;
const requestToken = Symbol(`layer-active-count:${this.getId()}`);
- const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
+ const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters(
syncContext.dataFilters,
this.getSource(),
this.getCurrentStyle()
@@ -305,6 +301,9 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
prevDataRequest: this.getDataRequest(dataRequestId),
nextMeta: searchFilters,
extentAware: source.isFilterByMapBounds(),
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => {
+ return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice);
+ },
});
let activeSource;
@@ -322,22 +321,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
let isSyncClustered;
try {
syncContext.startLoading(dataRequestId, requestToken, searchFilters);
- const abortController = new AbortController();
- syncContext.registerCancelCallback(requestToken, () => abortController.abort());
- const maxResultWindow = await this._documentSource.getMaxResultWindow();
- const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0);
- searchSource.setField('trackTotalHits', maxResultWindow + 1);
- const resp = await searchSource.fetch({
- abortSignal: abortController.signal,
- sessionId: syncContext.dataFilters.searchSessionId,
- legacyHitsTotal: false,
- });
- isSyncClustered = isTotalHitsGreaterThan(
- (resp.hits.total as unknown) as TotalHits,
- maxResultWindow
- );
- const countData = { isSyncClustered } as CountData;
- syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters);
+ isSyncClustered = !(await this._documentSource.canLoadAllDocuments(
+ searchFilters,
+ syncContext.registerCancelCallback.bind(null, requestToken)
+ ));
+ syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters);
} catch (error) {
if (!(error instanceof DataRequestAbortError) || !isSearchSourceAbortError(error)) {
syncContext.onLoadError(dataRequestId, requestToken, error.message);
diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
index 368ff8bebcdd1..d12c8432a4191 100644
--- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
+++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
@@ -111,6 +111,9 @@ export class HeatmapLayer extends AbstractLayer {
},
syncContext,
source: this.getSource(),
+ getUpdateDueToTimeslice: () => {
+ return true;
+ },
});
} catch (error) {
if (!(error instanceof DataRequestAbortError)) {
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index be113ab4cc2c9..ef41c157a2b17 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -36,6 +36,7 @@ import {
LayerDescriptor,
MapExtent,
StyleDescriptor,
+ Timeslice,
} from '../../../common/descriptor_types';
import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source';
import { DataRequestContext } from '../../actions';
@@ -78,7 +79,7 @@ export interface ILayer {
getMbLayerIds(): string[];
ownsMbLayerId(mbLayerId: string): boolean;
ownsMbSourceId(mbSourceId: string): boolean;
- syncLayerWithMB(mbMap: MbMap): void;
+ syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice): void;
getLayerTypeIconName(): string;
isInitialDataLoadComplete(): boolean;
getIndexPatternIds(): string[];
diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
index 6dba935ccc87d..2ad6a5ef73c6d 100644
--- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
@@ -21,6 +21,7 @@ import { VectorLayer, VectorLayerArguments } from '../vector_layer';
import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source';
import { DataRequestContext } from '../../../actions';
import {
+ Timeslice,
VectorLayerDescriptor,
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
@@ -66,7 +67,7 @@ export class TiledVectorLayer extends VectorLayer {
dataFilters,
}: DataRequestContext) {
const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`);
- const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
+ const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters(
dataFilters,
this.getSource(),
this._style as IVectorStyle
@@ -84,6 +85,10 @@ export class TiledVectorLayer extends VectorLayer {
source: this.getSource(),
prevDataRequest,
nextMeta: searchFilters,
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => {
+ // TODO use meta features to determine if tiles already contain features for timeslice.
+ return true;
+ },
});
const canSkip = noChangesInSourceState && noChangesInSearchState;
if (canSkip) {
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
index d305bb920b2ad..346e59f60af32 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
@@ -13,7 +13,13 @@ import {
SOURCE_DATA_REQUEST_ID,
VECTOR_SHAPE_TYPE,
} from '../../../../common/constants';
-import { MapExtent, MapQuery, VectorSourceRequestMeta } from '../../../../common/descriptor_types';
+import {
+ DataMeta,
+ MapExtent,
+ MapQuery,
+ Timeslice,
+ VectorSourceRequestMeta,
+} from '../../../../common/descriptor_types';
import { DataRequestContext } from '../../../actions';
import { IVectorSource } from '../../sources/vector_source';
import { DataRequestAbortError } from '../../util/data_request';
@@ -52,6 +58,7 @@ export async function syncVectorSource({
requestMeta,
syncContext,
source,
+ getUpdateDueToTimeslice,
}: {
layerId: string;
layerName: string;
@@ -59,6 +66,7 @@ export async function syncVectorSource({
requestMeta: VectorSourceRequestMeta;
syncContext: DataRequestContext;
source: IVectorSource;
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean;
}): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> {
const {
startLoading,
@@ -76,6 +84,7 @@ export async function syncVectorSource({
prevDataRequest,
nextMeta: requestMeta,
extentAware: source.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
if (canSkipFetch) {
return {
@@ -104,7 +113,14 @@ export async function syncVectorSource({
) {
layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection));
}
- stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta);
+ const responseMeta: DataMeta = meta ? { ...meta } : {};
+ if (requestMeta.applyGlobalTime && (await source.isTimeAware())) {
+ const timesiceMaskField = await source.getTimesliceMaskFieldName();
+ if (timesiceMaskField) {
+ responseMeta.timesiceMaskField = timesiceMaskField;
+ }
+ }
+ stopLoading(dataRequestId, requestToken, layerFeatureCollection, responseMeta);
return {
refreshed: true,
featureCollection: layerFeatureCollection,
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
index 8b4d25f4612cc..49a0878ef80b2 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
@@ -43,16 +43,19 @@ import {
getFillFilterExpression,
getLineFilterExpression,
getPointFilterExpression,
+ TimesliceMaskConfig,
} from '../../util/mb_filter_expressions';
import {
DynamicStylePropertyOptions,
MapFilters,
MapQuery,
+ Timeslice,
VectorJoinSourceRequestMeta,
VectorLayerDescriptor,
VectorSourceRequestMeta,
VectorStyleRequestMeta,
} from '../../../../common/descriptor_types';
+import { ISource } from '../../sources/source';
import { IVectorSource } from '../../sources/vector_source';
import { CustomIconAndTooltipContent, ILayer } from '../layer';
import { InnerJoin } from '../../joins/inner_join';
@@ -347,6 +350,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
prevDataRequest,
nextMeta: searchFilters,
extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource).
+ getUpdateDueToTimeslice: () => {
+ return true;
+ },
});
if (canSkipFetch) {
return {
@@ -389,17 +395,22 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return await Promise.all(joinSyncs);
}
- _getSearchFilters(
+ async _getSearchFilters(
dataFilters: MapFilters,
source: IVectorSource,
style: IVectorStyle
- ): VectorSourceRequestMeta {
+ ): Promise {
const fieldNames = [
...source.getFieldNames(),
...style.getSourceFieldNames(),
...this.getValidJoins().map((join) => join.getLeftField().getName()),
];
+ const timesliceMaskFieldName = await source.getTimesliceMaskFieldName();
+ if (timesliceMaskFieldName) {
+ fieldNames.push(timesliceMaskFieldName);
+ }
+
const sourceQuery = this.getQuery() as MapQuery;
return {
...dataFilters,
@@ -674,9 +685,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
layerId: this.getId(),
layerName: await this.getDisplayName(source),
prevDataRequest: this.getSourceDataRequest(),
- requestMeta: this._getSearchFilters(syncContext.dataFilters, source, style),
+ requestMeta: await this._getSearchFilters(syncContext.dataFilters, source, style),
syncContext,
source,
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => {
+ return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice);
+ },
});
await this._syncSupportsFeatureEditing({ syncContext, source });
if (
@@ -754,7 +768,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
}
}
- _setMbPointsProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbPointsProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const pointLayerId = this._getMbPointLayerId();
const symbolLayerId = this._getMbSymbolLayerId();
const pointLayer = mbMap.getLayer(pointLayerId);
@@ -771,7 +789,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
if (symbolLayer) {
mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none');
}
- this._setMbCircleProperties(mbMap, mvtSourceLayer);
+ this._setMbCircleProperties(mbMap, mvtSourceLayer, timesliceMaskConfig);
} else {
markerLayerId = symbolLayerId;
textLayerId = symbolLayerId;
@@ -779,7 +797,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none');
mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none');
}
- this._setMbSymbolProperties(mbMap, mvtSourceLayer);
+ this._setMbSymbolProperties(mbMap, mvtSourceLayer, timesliceMaskConfig);
}
this.syncVisibilityWithMb(mbMap, markerLayerId);
@@ -790,7 +808,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
}
}
- _setMbCircleProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbCircleProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const sourceId = this.getId();
const pointLayerId = this._getMbPointLayerId();
const pointLayer = mbMap.getLayer(pointLayerId);
@@ -822,7 +844,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.addLayer(mbLayer);
}
- const filterExpr = getPointFilterExpression(this.hasJoins());
+ const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig);
if (!_.isEqual(filterExpr, mbMap.getFilter(pointLayerId))) {
mbMap.setFilter(pointLayerId, filterExpr);
mbMap.setFilter(textLayerId, filterExpr);
@@ -841,7 +863,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
});
}
- _setMbSymbolProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbSymbolProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const sourceId = this.getId();
const symbolLayerId = this._getMbSymbolLayerId();
const symbolLayer = mbMap.getLayer(symbolLayerId);
@@ -858,7 +884,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.addLayer(mbLayer);
}
- const filterExpr = getPointFilterExpression(this.hasJoins());
+ const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig);
if (!_.isEqual(filterExpr, mbMap.getFilter(symbolLayerId))) {
mbMap.setFilter(symbolLayerId, filterExpr);
}
@@ -876,7 +902,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
});
}
- _setMbLinePolygonProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbLinePolygonProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const sourceId = this.getId();
const fillLayerId = this._getMbPolygonLayerId();
const lineLayerId = this._getMbLineLayerId();
@@ -940,14 +970,14 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
this.syncVisibilityWithMb(mbMap, fillLayerId);
mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom());
- const fillFilterExpr = getFillFilterExpression(hasJoins);
+ const fillFilterExpr = getFillFilterExpression(hasJoins, timesliceMaskConfig);
if (!_.isEqual(fillFilterExpr, mbMap.getFilter(fillLayerId))) {
mbMap.setFilter(fillLayerId, fillFilterExpr);
}
this.syncVisibilityWithMb(mbMap, lineLayerId);
mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom());
- const lineFilterExpr = getLineFilterExpression(hasJoins);
+ const lineFilterExpr = getLineFilterExpression(hasJoins, timesliceMaskConfig);
if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) {
mbMap.setFilter(lineLayerId, lineFilterExpr);
}
@@ -956,7 +986,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom());
}
- _setMbCentroidProperties(mbMap: MbMap, mvtSourceLayer?: string) {
+ _setMbCentroidProperties(
+ mbMap: MbMap,
+ mvtSourceLayer?: string,
+ timesliceMaskConfig?: TimesliceMaskConfig
+ ) {
const centroidLayerId = this._getMbCentroidLayerId();
const centroidLayer = mbMap.getLayer(centroidLayerId);
if (!centroidLayer) {
@@ -971,7 +1005,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.addLayer(mbLayer);
}
- const filterExpr = getCentroidFilterExpression(this.hasJoins());
+ const filterExpr = getCentroidFilterExpression(this.hasJoins(), timesliceMaskConfig);
if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) {
mbMap.setFilter(centroidLayerId, filterExpr);
}
@@ -986,17 +1020,32 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom());
}
- _syncStylePropertiesWithMb(mbMap: MbMap) {
- this._setMbPointsProperties(mbMap);
- this._setMbLinePolygonProperties(mbMap);
+ _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) {
+ const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice);
+ this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig);
+ this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig);
// centroid layers added after polygon layers to ensure they are on top of polygon layers
- this._setMbCentroidProperties(mbMap);
+ this._setMbCentroidProperties(mbMap, undefined, timesliceMaskConfig);
}
- syncLayerWithMB(mbMap: MbMap) {
+ _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined {
+ if (!timeslice || this.hasJoins()) {
+ return;
+ }
+
+ const prevMeta = this.getSourceDataRequest()?.getMeta();
+ return prevMeta !== undefined && prevMeta.timesiceMaskField !== undefined
+ ? {
+ timesiceMaskField: prevMeta.timesiceMaskField,
+ timeslice,
+ }
+ : undefined;
+ }
+
+ syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) {
addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap);
this._syncFeatureCollectionWithMb(mbMap);
- this._syncStylePropertiesWithMb(mbMap);
+ this._syncStylePropertiesWithMb(mbMap, timeslice);
}
_getMbPointLayerId() {
@@ -1094,6 +1143,15 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return await this._source.getLicensedFeatures();
}
+ _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) {
+ const prevDataRequest = this.getSourceDataRequest();
+ const prevMeta = prevDataRequest?.getMeta();
+ if (!prevMeta) {
+ return true;
+ }
+ return source.getUpdateDueToTimeslice(prevMeta, timeslice);
+ }
+
async addFeature(geometry: Geometry | Position[]) {
const layerSource = this.getSource();
await layerSource.addFeature(geometry);
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
index a51e291574b70..9f7bd1260ca22 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx
@@ -12,13 +12,19 @@ import { i18n } from '@kbn/i18n';
import { IFieldType, IndexPattern } from 'src/plugins/data/public';
import { GeoJsonProperties, Geometry, Position } from 'geojson';
import { AbstractESSource } from '../es_source';
-import { getHttp, getMapAppConfig, getSearchService } from '../../../kibana_services';
+import {
+ getHttp,
+ getMapAppConfig,
+ getSearchService,
+ getTimeFilter,
+} from '../../../kibana_services';
import {
addFieldToDSL,
getField,
hitsToGeoJson,
isTotalHitsGreaterThan,
PreIndexedShape,
+ TotalHits,
} from '../../../../common/elasticsearch_util';
// @ts-expect-error
import { UpdateSourceEditor } from './update_source_editor';
@@ -41,11 +47,14 @@ import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
import { ESDocField } from '../../fields/es_doc_field';
import { registerSource } from '../source_registry';
import {
+ DataMeta,
ESSearchSourceDescriptor,
+ Timeslice,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
+import { TimeRange } from '../../../../../../../src/plugins/data/common';
import { ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { IField } from '../../fields/field';
import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source';
@@ -59,6 +68,16 @@ import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_sou
import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source';
import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit';
+export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined {
+ const timeRangeBounds = getTimeFilter().calculateBounds(timerange);
+ return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined
+ ? {
+ from: timeRangeBounds.min.valueOf(),
+ to: timeRangeBounds.max.valueOf(),
+ }
+ : undefined;
+}
+
export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', {
defaultMessage: 'Documents',
});
@@ -338,7 +357,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
async _getSearchHits(
layerName: string,
searchFilters: VectorSourceRequestMeta,
- maxResultWindow: number,
registerCancelCallback: (callback: () => void) => void
) {
const indexPattern = await this.getIndexPattern();
@@ -350,8 +368,18 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
);
const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source
+
+ // Use Kibana global time extent instead of timeslice extent when all documents for global time extent can be loaded
+ // to allow for client-side masking of timeslice
+ const searchFiltersWithoutTimeslice = { ...searchFilters };
+ delete searchFiltersWithoutTimeslice.timeslice;
+ const useSearchFiltersWithoutTimeslice =
+ searchFilters.timeslice !== undefined &&
+ (await this.canLoadAllDocuments(searchFiltersWithoutTimeslice, registerCancelCallback));
+
+ const maxResultWindow = await this.getMaxResultWindow();
const searchSource = await this.makeSearchSource(
- searchFilters,
+ useSearchFiltersWithoutTimeslice ? searchFiltersWithoutTimeslice : searchFilters,
maxResultWindow,
initialSearchContext
);
@@ -375,11 +403,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
searchSessionId: searchFilters.searchSessionId,
});
+ const isTimeExtentForTimeslice =
+ searchFilters.timeslice !== undefined && !useSearchFiltersWithoutTimeslice;
return {
hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top
meta: {
resultsCount: resp.hits.hits.length,
areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length),
+ timeExtent: isTimeExtentForTimeslice
+ ? searchFilters.timeslice
+ : timerangeToTimeextent(searchFilters.timeFilters),
+ isTimeExtentForTimeslice,
},
};
}
@@ -424,16 +458,9 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
): Promise {
const indexPattern = await this.getIndexPattern();
- const indexSettings = await loadIndexSettings(indexPattern.title);
-
const { hits, meta } = this._isTopHits()
? await this._getTopHits(layerName, searchFilters, registerCancelCallback)
- : await this._getSearchHits(
- layerName,
- searchFilters,
- indexSettings.maxResultWindow,
- registerCancelCallback
- );
+ : await this._getSearchHits(layerName, searchFilters, registerCancelCallback);
const unusedMetaFields = indexPattern.metaFields.filter((metaField) => {
return !['_id', '_index'].includes(metaField);
@@ -743,6 +770,62 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
: urlTemplate,
};
}
+
+ async getTimesliceMaskFieldName(): Promise {
+ if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) {
+ return null;
+ }
+
+ const indexPattern = await this.getIndexPattern();
+ return indexPattern.timeFieldName ? indexPattern.timeFieldName : null;
+ }
+
+ getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean {
+ if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) {
+ return true;
+ }
+
+ if (
+ prevMeta.timeExtent === undefined ||
+ prevMeta.areResultsTrimmed === undefined ||
+ prevMeta.areResultsTrimmed
+ ) {
+ return true;
+ }
+
+ const isTimeExtentForTimeslice =
+ prevMeta.isTimeExtentForTimeslice !== undefined ? prevMeta.isTimeExtentForTimeslice : false;
+ if (!timeslice) {
+ return isTimeExtentForTimeslice
+ ? // Previous request only covers timeslice extent. Will need to re-fetch data to cover global time extent
+ true
+ : // Previous request covers global time extent.
+ // No need to re-fetch data since previous request already has data for the entire global time extent.
+ false;
+ }
+
+ const isWithin = isTimeExtentForTimeslice
+ ? timeslice.from >= prevMeta.timeExtent.from && timeslice.to <= prevMeta.timeExtent.to
+ : true;
+ return !isWithin;
+ }
+
+ async canLoadAllDocuments(
+ searchFilters: VectorSourceRequestMeta,
+ registerCancelCallback: (callback: () => void) => void
+ ) {
+ const abortController = new AbortController();
+ registerCancelCallback(() => abortController.abort());
+ const maxResultWindow = await this.getMaxResultWindow();
+ const searchSource = await this.makeSearchSource(searchFilters, 0);
+ searchSource.setField('trackTotalHits', maxResultWindow + 1);
+ const resp = await searchSource.fetch({
+ abortSignal: abortController.signal,
+ sessionId: searchFilters.searchSessionId,
+ legacyHitsTotal: false,
+ });
+ return !isTotalHitsGreaterThan((resp.hits.total as unknown) as TotalHits, maxResultWindow);
+ }
}
registerSource({
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
index d58e71db2a9ab..5bf7a2e47cc66 100644
--- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
@@ -228,6 +228,10 @@ export class MVTSingleLayerVectorSource
return tooltips;
}
+ async getTimesliceMaskFieldName() {
+ return null;
+ }
+
async supportsFeatureEditing(): Promise {
return false;
}
diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts
index 7a8fca337fd2e..0ecbde06cf3e2 100644
--- a/x-pack/plugins/maps/public/classes/sources/source.ts
+++ b/x-pack/plugins/maps/public/classes/sources/source.ts
@@ -13,7 +13,12 @@ import { GeoJsonProperties } from 'geojson';
import { copyPersistentState } from '../../reducers/copy_persistent_state';
import { IField } from '../fields/field';
import { FieldFormatter, LAYER_TYPE, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
-import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types';
+import {
+ AbstractSourceDescriptor,
+ Attribution,
+ DataMeta,
+ Timeslice,
+} from '../../../common/descriptor_types';
import { LICENSED_FEATURES } from '../../licensed_features';
import { PreIndexedShape } from '../../../common/elasticsearch_util';
@@ -64,6 +69,7 @@ export interface ISource {
getMinZoom(): number;
getMaxZoom(): number;
getLicensedFeatures(): Promise;
+ getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean;
}
export class AbstractSource implements ISource {
@@ -194,4 +200,8 @@ export class AbstractSource implements ISource {
async getLicensedFeatures(): Promise {
return [];
}
+
+ getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean {
+ return true;
+ }
}
diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
index 1194d571e344b..8f93de705e365 100644
--- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx
@@ -66,6 +66,7 @@ export interface IVectorSource extends ISource {
getSupportedShapeTypes(): Promise;
isBoundsAware(): boolean;
getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig;
+ getTimesliceMaskFieldName(): Promise;
supportsFeatureEditing(): Promise;
addFeature(geometry: Geometry | Position[]): Promise;
}
@@ -156,6 +157,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
return null;
}
+ async getTimesliceMaskFieldName(): Promise {
+ return null;
+ }
+
async addFeature(geometry: Geometry | Position[]) {
throw new Error('Should implement VectorSource#addFeature');
}
diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js
index c13b2fd441cad..da3cbb9055d43 100644
--- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js
+++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js
@@ -82,6 +82,9 @@ describe('updateDueToExtent', () => {
describe('canSkipSourceUpdate', () => {
const SOURCE_DATA_REQUEST_ID = 'foo';
+ const getUpdateDueToTimeslice = () => {
+ return true;
+ };
describe('isQueryAware', () => {
const queryAwareSourceMock = {
@@ -136,6 +139,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -156,6 +160,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -176,6 +181,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -193,6 +199,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -224,6 +231,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -244,6 +252,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -264,6 +273,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -281,6 +291,7 @@ describe('canSkipSourceUpdate', () => {
prevDataRequest,
nextMeta,
extentAware: queryAwareSourceMock.isFilterByMapBounds(),
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -327,6 +338,7 @@ describe('canSkipSourceUpdate', () => {
applyGlobalTime: false,
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -346,6 +358,7 @@ describe('canSkipSourceUpdate', () => {
applyGlobalTime: true,
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -375,6 +388,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -402,6 +416,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -429,6 +444,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -463,6 +479,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -498,6 +515,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -529,6 +547,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(false);
@@ -564,6 +583,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
@@ -599,6 +619,7 @@ describe('canSkipSourceUpdate', () => {
},
},
extentAware: false,
+ getUpdateDueToTimeslice,
});
expect(canSkipUpdate).toBe(true);
diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts
index 1f2678f40eecd..b6f03ef3d1c63 100644
--- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts
+++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts
@@ -10,7 +10,7 @@ import turfBboxPolygon from '@turf/bbox-polygon';
import turfBooleanContains from '@turf/boolean-contains';
import { isRefreshOnlyQuery } from './is_refresh_only_query';
import { ISource } from '../sources/source';
-import { DataMeta } from '../../../common/descriptor_types';
+import { DataMeta, Timeslice } from '../../../common/descriptor_types';
import { DataRequest } from './data_request';
const SOURCE_UPDATE_REQUIRED = true;
@@ -56,11 +56,13 @@ export async function canSkipSourceUpdate({
prevDataRequest,
nextMeta,
extentAware,
+ getUpdateDueToTimeslice,
}: {
source: ISource;
prevDataRequest: DataRequest | undefined;
nextMeta: DataMeta;
extentAware: boolean;
+ getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean;
}): Promise {
const timeAware = await source.isTimeAware();
const refreshTimerAware = await source.isRefreshTimerAware();
@@ -94,7 +96,9 @@ export async function canSkipSourceUpdate({
updateDueToApplyGlobalTime = prevMeta.applyGlobalTime !== nextMeta.applyGlobalTime;
if (nextMeta.applyGlobalTime) {
updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters);
- updateDueToTimeslice = !_.isEqual(prevMeta.timeslice, nextMeta.timeslice);
+ if (!_.isEqual(prevMeta.timeslice, nextMeta.timeslice)) {
+ updateDueToTimeslice = getUpdateDueToTimeslice(nextMeta.timeslice);
+ }
}
}
diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
index f5df741759cb3..6a193216c7c1e 100644
--- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
+++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts
@@ -12,67 +12,110 @@ import {
KBN_TOO_MANY_FEATURES_PROPERTY,
} from '../../../common/constants';
+import { Timeslice } from '../../../common/descriptor_types';
+
+export interface TimesliceMaskConfig {
+ timesiceMaskField: string;
+ timeslice: Timeslice;
+}
+
export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true];
const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];
-function getFilterExpression(geometryFilter: unknown[], hasJoins: boolean) {
- const filters: unknown[] = [
- EXCLUDE_TOO_MANY_FEATURES_BOX,
- EXCLUDE_CENTROID_FEATURES,
- geometryFilter,
- ];
+function getFilterExpression(
+ filters: unknown[],
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+) {
+ const allFilters: unknown[] = [...filters];
if (hasJoins) {
- filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]);
+ allFilters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]);
}
- return ['all', ...filters];
+ if (timesliceMaskConfig) {
+ allFilters.push(['has', timesliceMaskConfig.timesiceMaskField]);
+ allFilters.push([
+ '>=',
+ ['get', timesliceMaskConfig.timesiceMaskField],
+ timesliceMaskConfig.timeslice.from,
+ ]);
+ allFilters.push([
+ '<',
+ ['get', timesliceMaskConfig.timesiceMaskField],
+ timesliceMaskConfig.timeslice.to,
+ ]);
+ }
+
+ return ['all', ...allFilters];
}
-export function getFillFilterExpression(hasJoins: boolean): unknown[] {
+export function getFillFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
return getFilterExpression(
[
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
+ EXCLUDE_TOO_MANY_FEATURES_BOX,
+ EXCLUDE_CENTROID_FEATURES,
+ [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
+ ],
],
- hasJoins
+ hasJoins,
+ timesliceMaskConfig
);
}
-export function getLineFilterExpression(hasJoins: boolean): unknown[] {
+export function getLineFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
return getFilterExpression(
[
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
+ EXCLUDE_TOO_MANY_FEATURES_BOX,
+ EXCLUDE_CENTROID_FEATURES,
+ [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
+ ],
],
- hasJoins
+ hasJoins,
+ timesliceMaskConfig
);
}
-export function getPointFilterExpression(hasJoins: boolean): unknown[] {
+export function getPointFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
return getFilterExpression(
[
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
+ EXCLUDE_TOO_MANY_FEATURES_BOX,
+ EXCLUDE_CENTROID_FEATURES,
+ [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
+ ],
],
- hasJoins
+ hasJoins,
+ timesliceMaskConfig
);
}
-export function getCentroidFilterExpression(hasJoins: boolean): unknown[] {
- const filters: unknown[] = [
- EXCLUDE_TOO_MANY_FEATURES_BOX,
- ['==', ['get', KBN_IS_CENTROID_FEATURE], true],
- ];
-
- if (hasJoins) {
- filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]);
- }
-
- return ['all', ...filters];
+export function getCentroidFilterExpression(
+ hasJoins: boolean,
+ timesliceMaskConfig?: TimesliceMaskConfig
+): unknown[] {
+ return getFilterExpression(
+ [EXCLUDE_TOO_MANY_FEATURES_BOX, ['==', ['get', KBN_IS_CENTROID_FEATURE], true]],
+ hasJoins,
+ timesliceMaskConfig
+ );
}
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
index f0df797582bef..998329a78bfbb 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
@@ -11,7 +11,11 @@
import turfDistance from '@turf/distance';
// @ts-expect-error
import turfCircle from '@turf/circle';
-import { Position } from 'geojson';
+import { Feature, GeoJSON, Position } from 'geojson';
+
+const DRAW_CIRCLE_RADIUS = 'draw-circle-radius';
+
+export const DRAW_CIRCLE_RADIUS_MB_FILTER = ['==', 'meta', DRAW_CIRCLE_RADIUS];
export interface DrawCircleProperties {
center: Position;
@@ -22,10 +26,12 @@ type DrawCircleState = {
circle: {
properties: Omit & {
center: Position | null;
+ edge: Position | null;
+ radiusKm: number;
};
id: string | number;
incomingCoords: (coords: unknown[]) => void;
- toGeoJSON: () => unknown;
+ toGeoJSON: () => GeoJSON;
};
};
@@ -43,6 +49,7 @@ export const DrawCircle = {
type: 'Feature',
properties: {
center: null,
+ edge: null,
radiusKm: 0,
},
geometry: {
@@ -96,6 +103,7 @@ export const DrawCircle = {
}
const mouseLocation = [e.lngLat.lng, e.lngLat.lat];
+ state.circle.properties.edge = mouseLocation;
state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation);
const newCircleFeature = turfCircle(
state.circle.properties.center,
@@ -124,15 +132,53 @@ export const DrawCircle = {
this.changeMode('simple_select', {}, { silent: true });
}
},
- toDisplayFeatures(
- state: DrawCircleState,
- geojson: { properties: { active: string } },
- display: (geojson: unknown) => unknown
- ) {
- if (state.circle.properties.center) {
- geojson.properties.active = 'true';
- return display(geojson);
+ toDisplayFeatures(state: DrawCircleState, geojson: Feature, display: (geojson: Feature) => void) {
+ if (!state.circle.properties.center || !state.circle.properties.edge) {
+ return null;
+ }
+
+ geojson.properties!.active = 'true';
+
+ let radiusLabel = '';
+ if (state.circle.properties.radiusKm <= 1) {
+ radiusLabel = `${Math.round(state.circle.properties.radiusKm * 1000)} m`;
+ } else if (state.circle.properties.radiusKm <= 10) {
+ radiusLabel = `${state.circle.properties.radiusKm.toFixed(1)} km`;
+ } else {
+ radiusLabel = `${Math.round(state.circle.properties.radiusKm)} km`;
}
+
+ // display radius label, requires custom 'symbol' style with DRAW_CIRCLE_RADIUS_MB_FILTER filter
+ display({
+ type: 'Feature',
+ properties: {
+ meta: DRAW_CIRCLE_RADIUS,
+ parent: state.circle.id,
+ radiusLabel,
+ active: 'false',
+ },
+ geometry: {
+ type: 'Point',
+ coordinates: state.circle.properties.edge,
+ },
+ });
+
+ // display line from center vertex to edge
+ display({
+ type: 'Feature',
+ properties: {
+ meta: 'draw-circle-radius-line',
+ parent: state.circle.id,
+ active: 'true',
+ },
+ geometry: {
+ type: 'LineString',
+ coordinates: [state.circle.properties.center, state.circle.properties.edge],
+ },
+ });
+
+ // display circle
+ display(geojson);
},
onTrash(state: DrawCircleState) {
// @ts-ignore
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
index 879bd85dd6019..5d9cb59bbe522 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
@@ -14,9 +14,11 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { Feature } from 'geojson';
import { DRAW_SHAPE } from '../../../../common/constants';
-import { DrawCircle } from './draw_circle';
+import { DrawCircle, DRAW_CIRCLE_RADIUS_MB_FILTER } from './draw_circle';
import { DrawTooltip } from './draw_tooltip';
+const GL_DRAW_RADIUS_LABEL_LAYER_ID = 'gl-draw-radius-label';
+
const mbModeEquivalencies = new Map([
['simple_select', DRAW_SHAPE.SIMPLE_SELECT],
['draw_rectangle', DRAW_SHAPE.BOUNDS],
@@ -94,6 +96,7 @@ export class DrawControl extends Component {
this.props.mbMap.getCanvas().style.cursor = '';
this.props.mbMap.off('draw.modechange', this._onModeChange);
this.props.mbMap.off('draw.create', this._onDraw);
+ this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID);
this.props.mbMap.removeControl(this._mbDrawControl);
this._mbDrawControlAdded = false;
}
@@ -105,6 +108,25 @@ export class DrawControl extends Component {
if (!this._mbDrawControlAdded) {
this.props.mbMap.addControl(this._mbDrawControl);
+ this.props.mbMap.addLayer({
+ id: GL_DRAW_RADIUS_LABEL_LAYER_ID,
+ type: 'symbol',
+ source: 'mapbox-gl-draw-hot',
+ filter: DRAW_CIRCLE_RADIUS_MB_FILTER,
+ layout: {
+ 'text-anchor': 'right',
+ 'text-field': '{radiusLabel}',
+ 'text-size': 16,
+ 'text-offset': [-1, 0],
+ 'text-ignore-placement': true,
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': '#fbb03b',
+ 'text-halo-color': 'rgba(255, 255, 255, 1)',
+ 'text-halo-width': 2,
+ },
+ });
this._mbDrawControlAdded = true;
this.props.mbMap.getCanvas().style.cursor = 'crosshair';
this.props.mbMap.on('draw.modechange', this._onModeChange);
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts
index 4f94cbc7b7458..b9b4b184318f5 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts
@@ -27,6 +27,7 @@ import {
getMapSettings,
getScrollZoom,
getSpatialFiltersLayer,
+ getTimeslice,
} from '../../selectors/map_selectors';
import { getDrawMode, getIsFullScreen } from '../../selectors/ui_selectors';
import { getInspectorAdapters } from '../../reducers/non_serializable_instances';
@@ -43,6 +44,7 @@ function mapStateToProps(state: MapStoreState) {
inspectorAdapters: getInspectorAdapters(state),
scrollZoom: getScrollZoom(state),
isFullScreen: getIsFullScreen(state),
+ timeslice: getTimeslice(state),
featureModeActive:
getDrawMode(state) === DRAW_MODE.DRAW_SHAPES || getDrawMode(state) === DRAW_MODE.DRAW_POINTS,
filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS,
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx
index 96ff7b7dcf882..2ce4e2d98ce5f 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx
@@ -25,7 +25,7 @@ import { getInitialView } from './get_initial_view';
import { getPreserveDrawingBuffer } from '../../kibana_services';
import { ILayer } from '../../classes/layers/layer';
import { MapSettings } from '../../reducers/map';
-import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types';
+import { Goto, MapCenterAndZoom, Timeslice } from '../../../common/descriptor_types';
import {
DECIMAL_DEGREES_PRECISION,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
@@ -68,13 +68,12 @@ export interface Props {
onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
renderTooltipContent?: RenderToolTipContent;
setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void;
+ timeslice?: Timeslice;
featureModeActive: boolean;
filterModeActive: boolean;
}
interface State {
- prevLayerList: ILayer[] | undefined;
- hasSyncedLayerList: boolean;
mbMap: MapboxMap | undefined;
}
@@ -83,38 +82,23 @@ export class MBMap extends Component {
private _isMounted: boolean = false;
private _containerRef: HTMLDivElement | null = null;
private _prevDisableInteractive?: boolean;
+ private _prevLayerList?: ILayer[];
+ private _prevTimeslice?: Timeslice;
private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false });
private _tileStatusTracker?: TileStatusTracker;
state: State = {
- prevLayerList: undefined,
- hasSyncedLayerList: false,
mbMap: undefined,
};
- static getDerivedStateFromProps(nextProps: Props, prevState: State) {
- 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();
- }
+ this._syncMbMapWithMapState(); // do not debounce syncing of map-state
+ this._debouncedSync();
}
componentWillUnmount() {
@@ -134,16 +118,13 @@ export class MBMap extends Component {
_debouncedSync = _.debounce(() => {
if (this._isMounted && this.props.isMapReady && this.state.mbMap) {
- if (!this.state.hasSyncedLayerList) {
- this.setState(
- {
- hasSyncedLayerList: true,
- },
- () => {
- this._syncMbMapWithLayerList();
- this._syncMbMapWithInspector();
- }
- );
+ const hasLayerListChanged = this._prevLayerList !== this.props.layerList; // Comparing re-select memoized instance so no deep equals needed
+ const hasTimesliceChanged = !_.isEqual(this._prevTimeslice, this.props.timeslice);
+ if (hasLayerListChanged || hasTimesliceChanged) {
+ this._prevLayerList = this.props.layerList;
+ this._prevTimeslice = this.props.timeslice;
+ this._syncMbMapWithLayerList();
+ this._syncMbMapWithInspector();
}
this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap);
this._syncSettings();
@@ -346,7 +327,9 @@ export class MBMap extends Component {
this.props.layerList,
this.props.spatialFiltersLayer
);
- this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap!));
+ this.props.layerList.forEach((layer) =>
+ layer.syncLayerWithMB(this.state.mbMap!, this.props.timeslice)
+ );
syncLayerOrder(this.state.mbMap, this.props.spatialFiltersLayer, this.props.layerList);
};
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
index 5a477754683e6..509cece671dd6 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
@@ -54,9 +54,9 @@ import {
} from '../selectors/map_selectors';
import {
APP_ID,
- getExistingMapPath,
+ getEditPath,
+ getFullPath,
MAP_SAVED_OBJECT_TYPE,
- MAP_PATH,
RawValue,
} from '../../common/constants';
import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
@@ -180,13 +180,13 @@ export class MapEmbeddable
: '';
const input = this.getInput();
const title = input.hidePanelTitles ? '' : input.title || savedMapTitle;
- const savedObjectId = (input as MapByReferenceInput).savedObjectId;
+ const savedObjectId = 'savedObjectId' in input ? input.savedObjectId : undefined;
this.updateOutput({
...this.getOutput(),
defaultTitle: savedMapTitle,
title,
- editPath: `/${MAP_PATH}/${savedObjectId}`,
- editUrl: getHttp().basePath.prepend(getExistingMapPath(savedObjectId)),
+ editPath: getEditPath(savedObjectId),
+ editUrl: getHttp().basePath.prepend(getFullPath(savedObjectId)),
indexPatterns: await this._getIndexPatterns(),
});
}
diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
index eff49c1b1242e..cc0ed19db0b40 100644
--- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
+++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
@@ -6,40 +6,22 @@
*/
import { suggestEMSTermJoinConfig } from './ems_autosuggest';
-import { FeatureCollection } from 'geojson';
class MockFileLayer {
- private readonly _url: string;
private readonly _id: string;
private readonly _fields: Array<{ id: string }>;
- constructor(url: string, fields: Array<{ id: string }>) {
- this._url = url;
- this._id = url;
+ constructor(id: string, fields: Array<{ id: string; alias?: string[]; values?: string[] }>) {
+ this._id = id;
this._fields = fields;
}
- getFields() {
- return this._fields;
+ getId() {
+ return this._id;
}
- getGeoJson() {
- if (this._url === 'world_countries') {
- return ({
- type: 'FeatureCollection',
- features: [
- { properties: { iso2: 'CA', iso3: 'CAN' } },
- { properties: { iso2: 'US', iso3: 'USA' } },
- ],
- } as unknown) as FeatureCollection;
- } else if (this._url === 'zips') {
- return ({
- type: 'FeatureCollection',
- features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }],
- } as unknown) as FeatureCollection;
- } else {
- throw new Error(`unrecognized mock url ${this._url}`);
- }
+ getFields() {
+ return this._fields;
}
hasId(id: string) {
@@ -51,31 +33,31 @@ jest.mock('../util', () => {
return {
async getEmsFileLayers() {
return [
- new MockFileLayer('world_countries', [{ id: 'iso2' }, { id: 'iso3' }]),
- new MockFileLayer('zips', [{ id: 'zip' }]),
+ new MockFileLayer('world_countries', [
+ {
+ id: 'iso2',
+ alias: ['(geo\\.){0,}country_iso_code$', '(country|countries)'],
+ values: ['CA', 'US'],
+ },
+ { id: 'iso3', values: ['CAN', 'USA'] },
+ { id: 'name', alias: ['(country|countries)'] },
+ ]),
+ new MockFileLayer('usa_zip_codes', [
+ { id: 'zip', alias: ['zip'], values: ['40204', '40205'] },
+ ]),
];
},
};
});
describe('suggestEMSTermJoinConfig', () => {
- test('no info provided', async () => {
+ test('Should not validate when no info provided', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({});
expect(termJoinConfig).toBe(null);
});
- describe('validate common column names', () => {
- test('ecs region', async () => {
- const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValuesColumnName: 'destination.geo.region_iso_code',
- });
- expect(termJoinConfig).toEqual({
- layerId: 'administrative_regions_lvl2',
- field: 'region_iso_code',
- });
- });
-
- test('ecs country', async () => {
+ describe('With common column names', () => {
+ test('should match first match', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
sampleValuesColumnName: 'country_iso_code',
});
@@ -85,78 +67,61 @@ describe('suggestEMSTermJoinConfig', () => {
});
});
- test('country', async () => {
+ test('When sampleValues are provided, should reject match if no sampleValues for a layer, even though the name matches', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValuesColumnName: 'Country_name',
- });
- expect(termJoinConfig).toEqual({
- layerId: 'world_countries',
- field: 'name',
+ sampleValuesColumnName: 'country_iso_code',
+ sampleValues: ['FO', 'US', 'CA'],
});
+ expect(termJoinConfig).toEqual(null);
});
- test('unknown name', async () => {
+ test('should reject match if sampleValues not in id-list', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValuesColumnName: 'cntry',
+ sampleValuesColumnName: 'zip',
+ sampleValues: ['90201', '40205'],
});
expect(termJoinConfig).toEqual(null);
});
- });
- describe('validate well known formats', () => {
- test('5-digit zip code', async () => {
+ test('should return first match (regex matches both iso2 and name)', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['90201', 40204],
+ sampleValuesColumnName: 'Country_name',
});
expect(termJoinConfig).toEqual({
- layerId: 'usa_zip_codes',
- field: 'zip',
+ layerId: 'world_countries',
+ field: 'iso2',
});
});
- test('mismatch', async () => {
+ test('unknown name', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['90201', 'foobar'],
+ sampleValuesColumnName: 'cntry',
});
expect(termJoinConfig).toEqual(null);
});
});
- describe('validate based on EMS data', () => {
- test('Should validate with zip codes layer', async () => {
+ describe('validate well known formats (using id-values in manifest)', () => {
+ test('Should validate known zipcodes', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['40204', 40205],
- emsLayerIds: ['world_countries', 'zips'],
+ sampleValues: ['40205', 40204],
});
expect(termJoinConfig).toEqual({
- layerId: 'zips',
+ layerId: 'usa_zip_codes',
field: 'zip',
});
});
- test('Should not validate with faulty zip codes', async () => {
+ test('Should not validate unknown zipcode (in this case, 90201)', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['40204', '00000'],
- emsLayerIds: ['world_countries', 'zips'],
+ sampleValues: ['90201', 40204],
});
expect(termJoinConfig).toEqual(null);
});
- test('Should validate against countries', async () => {
+ test('Should not validate mismatches', async () => {
const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['USA', 'USA', 'CAN'],
- emsLayerIds: ['world_countries', 'zips'],
- });
- expect(termJoinConfig).toEqual({
- layerId: 'world_countries',
- field: 'iso3',
- });
- });
-
- test('Should not validate against missing countries', async () => {
- const termJoinConfig = await suggestEMSTermJoinConfig({
- sampleValues: ['USA', 'BEL', 'CAN'],
- emsLayerIds: ['world_countries', 'zips'],
+ sampleValues: ['90201', 'foobar'],
});
expect(termJoinConfig).toEqual(null);
});
diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
index 952e48a71a9dc..66fcbd805f53e 100644
--- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
+++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
@@ -7,10 +7,8 @@
import type { FileLayer } from '@elastic/ems-client';
import { getEmsFileLayers } from '../util';
-import { emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common';
export interface SampleValuesConfig {
- emsLayerIds?: string[];
sampleValues?: Array;
sampleValuesColumnName?: string;
}
@@ -20,44 +18,16 @@ export interface EMSTermJoinConfig {
field: string;
}
-const wellKnownColumnNames = [
- {
- regex: /(geo\.){0,}country_iso_code$/i, // ECS postfix for country
- emsConfig: {
- layerId: emsWorldLayerId,
- field: 'iso2',
- },
- },
- {
- regex: /(geo\.){0,}region_iso_code$/i, // ECS postfixn for region
- emsConfig: {
- layerId: emsRegionLayerId,
- field: 'region_iso_code',
- },
- },
- {
- regex: /^country/i, // anything starting with country
- emsConfig: {
- layerId: emsWorldLayerId,
- field: 'name',
- },
- },
-];
-
-const wellKnownColumnFormats = [
- {
- regex: /(^\d{5}$)/i, // 5-digit zipcode
- emsConfig: {
- layerId: emsUsaZipLayerId,
- field: 'zip',
- },
- },
-];
-
interface UniqueMatch {
- config: { layerId: string; field: string };
+ config: EMSTermJoinConfig;
count: number;
}
+interface FileLayerFieldShim {
+ id: string;
+ values?: string[];
+ regex?: string;
+ alias?: string[];
+}
export async function suggestEMSTermJoinConfig(
sampleValuesConfig: SampleValuesConfig
@@ -65,20 +35,17 @@ export async function suggestEMSTermJoinConfig(
const matches: EMSTermJoinConfig[] = [];
if (sampleValuesConfig.sampleValuesColumnName) {
- matches.push(...suggestByName(sampleValuesConfig.sampleValuesColumnName));
+ const matchesBasedOnColumnName = await suggestByName(
+ sampleValuesConfig.sampleValuesColumnName,
+ sampleValuesConfig.sampleValues
+ );
+ matches.push(...matchesBasedOnColumnName);
}
if (sampleValuesConfig.sampleValues && sampleValuesConfig.sampleValues.length) {
- if (sampleValuesConfig.emsLayerIds && sampleValuesConfig.emsLayerIds.length) {
- matches.push(
- ...(await suggestByEMSLayerIds(
- sampleValuesConfig.emsLayerIds,
- sampleValuesConfig.sampleValues
- ))
- );
- } else {
- matches.push(...suggestByValues(sampleValuesConfig.sampleValues));
- }
+ // Only looks at id-values in main manifest
+ const matchesBasedOnIds = await suggestByIdValues(sampleValuesConfig.sampleValues);
+ matches.push(...matchesBasedOnIds);
}
const uniqMatches: UniqueMatch[] = matches.reduce((accum: UniqueMatch[], match) => {
@@ -105,92 +72,80 @@ export async function suggestEMSTermJoinConfig(
return uniqMatches.length ? uniqMatches[0].config : null;
}
-function suggestByName(columnName: string): EMSTermJoinConfig[] {
- const matches = wellKnownColumnNames.filter((wellknown) => {
- return columnName.match(wellknown.regex);
- });
-
- return matches.map((m) => {
- return m.emsConfig;
- });
-}
+async function suggestByName(
+ columnName: string,
+ sampleValues?: Array
+): Promise {
+ const fileLayers = await getEmsFileLayers();
-function suggestByValues(values: Array): EMSTermJoinConfig[] {
- const matches = wellKnownColumnFormats.filter((wellknown) => {
- for (let i = 0; i < values.length; i++) {
- const value = values[i].toString();
- if (!value.match(wellknown.regex)) {
- return false;
+ const matches: EMSTermJoinConfig[] = [];
+ fileLayers.forEach((fileLayer) => {
+ const emsFields: FileLayerFieldShim[] = fileLayer.getFields();
+ emsFields.forEach((emsField: FileLayerFieldShim) => {
+ if (!emsField.alias || !emsField.alias.length) {
+ return;
}
- }
- return true;
- });
- return matches.map((m) => {
- return m.emsConfig;
+ const emsConfig = {
+ layerId: fileLayer.getId(),
+ field: emsField.id,
+ };
+ emsField.alias.forEach((alias: string) => {
+ const regex = new RegExp(alias, 'i');
+ const nameMatchesAlias = !!columnName.match(regex);
+ // Check if this violates any known id-values.
+
+ let isMatch: boolean;
+ if (sampleValues) {
+ if (emsField.values && emsField.values.length) {
+ isMatch = nameMatchesAlias && allSamplesMatch(sampleValues, emsField.values);
+ } else {
+ // requires validation against sample-values but EMS provides no meta to do so.
+ isMatch = false;
+ }
+ } else {
+ isMatch = nameMatchesAlias;
+ }
+
+ if (isMatch) {
+ matches.push(emsConfig);
+ }
+ });
+ });
});
-}
-function existsInEMS(emsJson: any, emsFieldId: string, sampleValue: string): boolean {
- for (let i = 0; i < emsJson.features.length; i++) {
- const emsFieldValue = emsJson.features[i].properties[emsFieldId].toString();
- if (emsFieldValue.toString() === sampleValue) {
- return true;
- }
- }
- return false;
+ return matches;
}
-function matchesEmsField(emsJson: any, emsFieldId: string, sampleValues: Array) {
+function allSamplesMatch(sampleValues: Array, ids: string[]) {
for (let j = 0; j < sampleValues.length; j++) {
const sampleValue = sampleValues[j].toString();
- if (!existsInEMS(emsJson, emsFieldId, sampleValue)) {
+ if (!ids.includes(sampleValue)) {
return false;
}
}
return true;
}
-async function getMatchesForEMSLayer(
- emsLayerId: string,
+async function suggestByIdValues(
sampleValues: Array
): Promise {
+ const matches: EMSTermJoinConfig[] = [];
const fileLayers: FileLayer[] = await getEmsFileLayers();
- const emsFileLayer: FileLayer | undefined = fileLayers.find((fl: FileLayer) =>
- fl.hasId(emsLayerId)
- );
-
- if (!emsFileLayer) {
- return [];
- }
-
- const emsFields = emsFileLayer.getFields();
-
- try {
- const emsJson = await emsFileLayer.getGeoJson();
- const matches: EMSTermJoinConfig[] = [];
- for (let f = 0; f < emsFields.length; f++) {
- if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) {
- matches.push({
- layerId: emsLayerId,
- field: emsFields[f].id,
- });
+ fileLayers.forEach((fileLayer) => {
+ const emsFields: FileLayerFieldShim[] = fileLayer.getFields();
+ emsFields.forEach((emsField: FileLayerFieldShim) => {
+ if (!emsField.values || !emsField.values.length) {
+ return;
}
- }
- return matches;
- } catch (e) {
- return [];
- }
-}
-
-async function suggestByEMSLayerIds(
- emsLayerIds: string[],
- values: Array
-): Promise {
- const matches = [];
- for (const emsLayerId of emsLayerIds) {
- const layerIdMathes = await getMatchesForEMSLayer(emsLayerId, values);
- matches.push(...layerIdMathes);
- }
+ const emsConfig = {
+ layerId: fileLayer.getId(),
+ field: emsField.id,
+ };
+ if (allSamplesMatch(sampleValues, emsField.values)) {
+ matches.push(emsConfig);
+ }
+ });
+ });
return matches;
}
diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx
index 0dfff5a2c221e..92459ed28ab91 100644
--- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx
+++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx
@@ -44,7 +44,7 @@ import { getTopNavConfig } from '../top_nav_config';
import { MapQuery } from '../../../../common/descriptor_types';
import { goToSpecifiedPath } from '../../../render_app';
import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type';
-import { getExistingMapPath, APP_ID } from '../../../../common/constants';
+import { getFullPath, APP_ID } from '../../../../common/constants';
import {
getInitialQuery,
getInitialRefreshConfig,
@@ -356,7 +356,7 @@ export class MapApp extends React.Component {
const savedObjectId = this.props.savedMap.getSavedObjectId();
if (savedObjectId) {
getCoreChrome().recentlyAccessed.add(
- getExistingMapPath(savedObjectId),
+ getFullPath(savedObjectId),
this.props.savedMap.getTitle(),
savedObjectId
);
diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts
index f7e0012fdd9c2..45d3e0352acf6 100644
--- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts
+++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts
@@ -241,6 +241,10 @@ export class SavedMap {
return this._originatingApp;
}
+ public getOriginatingAppName(): string | undefined {
+ return this._originatingApp ? this.getAppNameFromId(this._originatingApp) : undefined;
+ }
+
public getAppNameFromId = (appId: string): string | undefined => {
return this._getStateTransfer().getAppNameFromId(appId);
};
diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx
index 7ac8c3070eb9d..79bc820d67b46 100644
--- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx
+++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx
@@ -151,9 +151,8 @@ export function getTopNavConfig({
const saveModalProps = {
onSave: async (
props: OnSaveProps & {
- returnToOrigin?: boolean;
dashboardId?: string | null;
- addToLibrary?: boolean;
+ addToLibrary: boolean;
}
) => {
try {
@@ -181,7 +180,7 @@ export function getTopNavConfig({
await savedMap.save({
...props,
newTags: selectedTags,
- saveByReference: Boolean(props.addToLibrary),
+ saveByReference: props.addToLibrary,
});
// showSaveModal wrapper requires onSave to return an object with an id to close the modal after successful save
return { id: 'id' };
@@ -204,8 +203,19 @@ export function getTopNavConfig({
saveModal = (
{
+ return saveModalProps.onSave({ ...props, addToLibrary: true });
+ }}
originatingApp={savedMap.getOriginatingApp()}
getAppNameFromId={savedMap.getAppNameFromId}
+ returnToOriginSwitchLabel={
+ savedMap.isByValue()
+ ? i18n.translate('xpack.maps.topNav.updatePanel', {
+ defaultMessage: 'Update panel on {originatingAppName}',
+ values: { originatingAppName: savedMap.getOriginatingAppName() },
+ })
+ : undefined
+ }
options={tagSelector}
/>
);
diff --git a/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts b/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts
index 268e5fa600b46..f05836dff2bd9 100644
--- a/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts
+++ b/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts
@@ -60,7 +60,9 @@ export function startAppStateSyncing(appStateManager: AppStateManager) {
stateContainer.set(initialAppState);
// set current url to whatever is in app state container
- kbnUrlStateStorage.set('_a', initialAppState);
+ kbnUrlStateStorage.set('_a', initialAppState, {
+ replace: true,
+ });
// finally start syncing state containers with url
startSyncingAppStateWithUrl();
diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts
index c753297932037..b8676559a4e2b 100644
--- a/x-pack/plugins/maps/server/plugin.ts
+++ b/x-pack/plugins/maps/server/plugin.ts
@@ -22,7 +22,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js';
// @ts-ignore
import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js';
import { registerMapsUsageCollector } from './maps_telemetry/collectors/register';
-import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants';
+import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getFullPath } from '../common/constants';
import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects';
import { MapsXPackConfig } from '../config';
// @ts-ignore
@@ -77,7 +77,7 @@ export class MapsPlugin implements Plugin {
home.sampleData.addAppLinksToSampleDataset('ecommerce', [
{
- path: getExistingMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'),
+ path: getFullPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: APP_ICON,
},
@@ -99,7 +99,7 @@ export class MapsPlugin implements Plugin {
home.sampleData.addAppLinksToSampleDataset('flights', [
{
- path: getExistingMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'),
+ path: getFullPath('5dd88580-1906-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: APP_ICON,
},
@@ -120,7 +120,7 @@ export class MapsPlugin implements Plugin {
home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects());
home.sampleData.addAppLinksToSampleDataset('logs', [
{
- path: getExistingMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'),
+ path: getFullPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: APP_ICON,
},
diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts
index 78f70e27b2b7b..24effd651a31b 100644
--- a/x-pack/plugins/maps/server/saved_objects/map.ts
+++ b/x-pack/plugins/maps/server/saved_objects/map.ts
@@ -6,7 +6,7 @@
*/
import { SavedObjectsType } from 'src/core/server';
-import { APP_ICON, getExistingMapPath } from '../../common/constants';
+import { APP_ICON, getFullPath } from '../../common/constants';
// @ts-ignore
import { savedObjectMigrations } from './saved_object_migrations';
@@ -34,7 +34,7 @@ export const mapSavedObjects: SavedObjectsType = {
},
getInAppUrl(obj) {
return {
- path: getExistingMapPath(obj.id),
+ path: getFullPath(obj.id),
uiCapabilitiesPath: 'maps.show',
};
},
diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts
index 410c833b8ac77..3c63850f87291 100644
--- a/x-pack/plugins/maps/server/tutorials/ems/index.ts
+++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts
@@ -16,6 +16,48 @@ export function emsBoundariesSpecProvider({
emsLandingPageUrl: string;
prependBasePath: (path: string) => string;
}) {
+ const instructions = {
+ instructionSets: [
+ {
+ instructionVariants: [
+ {
+ id: 'EMS',
+ instructions: [
+ {
+ title: i18n.translate('xpack.maps.tutorials.ems.downloadStepTitle', {
+ defaultMessage: 'Download Elastic Maps Service boundaries',
+ }),
+ textPre: i18n.translate('xpack.maps.tutorials.ems.downloadStepText', {
+ defaultMessage:
+ '1. Navigate to Elastic Maps Service [landing page]({emsLandingPageUrl}/).\n\
+2. In the left sidebar, select an administrative boundary.\n\
+3. Click `Download GeoJSON` button.',
+ values: {
+ emsLandingPageUrl,
+ },
+ }),
+ },
+ {
+ title: i18n.translate('xpack.maps.tutorials.ems.uploadStepTitle', {
+ defaultMessage: 'Index Elastic Maps Service boundaries',
+ }),
+ textPre: i18n.translate('xpack.maps.tutorials.ems.uploadStepText', {
+ defaultMessage:
+ '1. Open [Maps]({newMapUrl}).\n\
+2. Click `Add layer`, then select `Upload GeoJSON`.\n\
+3. Upload the GeoJSON file and click `Import file`.',
+ values: {
+ newMapUrl: prependBasePath(getNewMapPath()),
+ },
+ }),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
return () => ({
id: 'emsBoundaries',
name: i18n.translate('xpack.maps.tutorials.ems.nameTitle', {
@@ -34,46 +76,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou
euiIconType: 'emsApp',
completionTimeMinutes: 1,
previewImagePath: '/plugins/maps/assets/boundaries_screenshot.png',
- onPrem: {
- instructionSets: [
- {
- instructionVariants: [
- {
- id: 'EMS',
- instructions: [
- {
- title: i18n.translate('xpack.maps.tutorials.ems.downloadStepTitle', {
- defaultMessage: 'Download Elastic Maps Service boundaries',
- }),
- textPre: i18n.translate('xpack.maps.tutorials.ems.downloadStepText', {
- defaultMessage:
- '1. Navigate to Elastic Maps Service [landing page]({emsLandingPageUrl}).\n\
-2. In the left sidebar, select an administrative boundary.\n\
-3. Click `Download GeoJSON` button.',
- values: {
- emsLandingPageUrl,
- },
- }),
- },
- {
- title: i18n.translate('xpack.maps.tutorials.ems.uploadStepTitle', {
- defaultMessage: 'Index Elastic Maps Service boundaries',
- }),
- textPre: i18n.translate('xpack.maps.tutorials.ems.uploadStepText', {
- defaultMessage:
- '1. Open [Maps]({newMapUrl}).\n\
-2. Click `Add layer`, then select `Upload GeoJSON`.\n\
-3. Upload the GeoJSON file and click `Import file`.',
- values: {
- newMapUrl: prependBasePath(getNewMapPath()),
- },
- }),
- },
- ],
- },
- ],
- },
- ],
- },
+ onPrem: instructions,
+ elasticCloud: instructions,
});
}
diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts
index fa40cefcaed48..74d3286438588 100644
--- a/x-pack/plugins/ml/common/types/results.ts
+++ b/x-pack/plugins/ml/common/types/results.ts
@@ -6,6 +6,7 @@
*/
import { estypes } from '@elastic/elasticsearch';
+import { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts';
export interface GetStoppedPartitionResult {
jobs: string[] | Record;
@@ -13,6 +14,9 @@ export interface GetStoppedPartitionResult {
export interface GetDatafeedResultsChartDataResult {
bucketResults: number[][];
datafeedResults: number[][];
+ annotationResultsRect: RectAnnotationDatum[];
+ annotationResultsLine: LineAnnotationDatum[];
+ modelSnapshotResultsLine: LineAnnotationDatum[];
}
export interface DatafeedResultsChartDataParams {
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index e3bcf307e6f00..7b3f457106033 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -27,7 +27,6 @@
"management",
"licenseManagement",
"maps",
- "lens",
"usageCollection"
],
"server": true,
diff --git a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts
index 0907cce832bf8..f16ba27524670 100644
--- a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts
+++ b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts
@@ -9,7 +9,6 @@ import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/publi
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { kibanaLegacyPluginMock } from '../../../../../src/plugins/kibana_legacy/public/mocks';
import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks';
-import { lensPluginMock } from '../../../lens/public/mocks';
import { triggersActionsUiMock } from '../../../triggers_actions_ui/public/mocks';
export const createMlStartDepsMock = () => ({
@@ -22,7 +21,6 @@ export const createMlStartDepsMock = () => ({
spaces: jest.fn(),
embeddable: embeddablePluginMock.createStartContract(),
maps: jest.fn(),
- lens: lensPluginMock.createStartContract(),
triggersActionsUi: triggersActionsUiMock.createStart(),
dataVisualizer: jest.fn(),
});
diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx
index 8be513f372e56..222d23acb40a7 100644
--- a/x-pack/plugins/ml/public/application/app.tsx
+++ b/x-pack/plugins/ml/public/application/app.tsx
@@ -77,7 +77,6 @@ const App: FC = ({ coreStart, deps, appMountParams }) => {
data: deps.data,
security: deps.security,
licenseManagement: deps.licenseManagement,
- lens: deps.lens,
storage: localStorage,
embeddable: deps.embeddable,
maps: deps.maps,
diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
index afed7e79ff757..b68e64a5d9f6a 100644
--- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
+++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
@@ -494,13 +494,13 @@ class AnnotationsTableUI extends Component {
render: (annotation) => {
const viewDataFeedText = (
);
const viewDataFeedTooltipAriaLabelText = i18n.translate(
- 'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel',
- { defaultMessage: 'View datafeed' }
+ 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel',
+ { defaultMessage: 'Datafeed chart' }
);
return (
) : null}
diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
index 841f0d03fa21c..1ade617fa60a5 100644
--- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
@@ -19,7 +19,6 @@ import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/p
import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public';
import type { MapsStartApi } from '../../../../../maps/public';
import type { DataVisualizerPluginStart } from '../../../../../data_visualizer/public';
-import type { LensPublicStart } from '../../../../../lens/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
interface StartPlugins {
@@ -29,7 +28,6 @@ interface StartPlugins {
share: SharePluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
- lens?: LensPublicStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
dataVisualizer?: DataVisualizerPluginStart;
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx
index d24ec2126aee8..766f1bda64d5e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx
@@ -22,7 +22,6 @@ import {
EuiFlyoutHeader,
EuiForm,
EuiFormRow,
- EuiOverlayMask,
EuiSelect,
EuiTitle,
} from '@elastic/eui';
@@ -129,188 +128,180 @@ export const EditActionFlyout: FC> = ({ closeFlyout, item }
};
return (
-
-
-
-
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', {
- defaultMessage: 'Edit {jobId}',
- values: {
- jobId,
- },
- })}
-
-
-
-
-
-
+
+
+
+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', {
+ defaultMessage: 'Edit {jobId}',
+ values: {
+ jobId,
+ },
+ })}
+
+
+
+
+
+
+
- ) =>
- setAllowLazyStart(e.target.value)
- }
- />
-
- ) =>
+ setAllowLazyStart(e.target.value)
+ }
+ />
+
+
+ setDescription(e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel',
+ {
+ defaultMessage: 'Update the job description.',
}
)}
- >
- setDescription(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel',
- {
- defaultMessage: 'Update the job description.',
- }
- )}
- />
-
-
+
+
+ setModelMemoryLimit(e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel',
{
- defaultMessage: 'Model memory limit',
+ defaultMessage: 'Update the model memory limit.',
}
)}
- isInvalid={mmlValidationError !== undefined}
- error={mmlValidationError}
- >
- setModelMemoryLimit(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel',
- {
- defaultMessage: 'Update the model memory limit.',
- }
- )}
- />
-
-
+
+
+
+ setMaxNumThreads(e.target.value === '' ? undefined : +e.target.value)
}
+ step={1}
+ min={1}
+ readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED}
+ value={maxNumThreads}
+ />
+
+
+
+
+
+
+
+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+
+
-
- setMaxNumThreads(e.target.value === '' ? undefined : +e.target.value)
- }
- step={1}
- min={1}
- readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED}
- value={maxNumThreads}
- />
-
-
-
-
-
-
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', {
- defaultMessage: 'Cancel',
- })}
-
-
-
-
- {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', {
- defaultMessage: 'Update',
- })}
-
-
-
-
-
-
+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', {
+ defaultMessage: 'Update',
+ })}
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx
index 88ffaa0da7fdc..93be45bbdaf97 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx
@@ -114,10 +114,7 @@ export const ExpandedRow: FC = ({ item }) => {
}
const {
- services: {
- share,
- application: { navigateToUrl },
- },
+ services: { share },
} = useMlKibana();
const tabs = [
@@ -402,17 +399,16 @@ export const ExpandedRow: FC = ({ item }) => {
{
- const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator(
- 'INGEST_PIPELINES_APP_URL_GENERATOR'
- );
- await navigateToUrl(
- await ingestPipelinesAppUrlGenerator.createUrl({
- page: 'pipeline_edit',
- pipelineId: pipelineName,
- absolute: true,
- })
+ onClick={() => {
+ const locator = share.url.locators.get(
+ 'INGEST_PIPELINES_APP_LOCATOR'
);
+ if (!locator) return;
+ locator.navigate({
+ page: 'pipeline_edit',
+ pipelineId: pipelineName,
+ absolute: true,
+ });
}}
>
= ({ anomalies, jobIds }) => {
}
const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({
- emsLayerIds: COMMON_EMS_LAYER_IDS,
sampleValues: Array.from(entityValues),
sampleValuesColumnName: entityName || '',
});
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
index 71f3795518bc9..b3b9487523196 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
@@ -15,7 +15,7 @@ export const CHART_DIRECTION = {
export type ChartDirectionType = typeof CHART_DIRECTION[keyof typeof CHART_DIRECTION];
// [width, height]
-export const CHART_SIZE: ChartSizeArray = ['100%', 300];
+export const CHART_SIZE: ChartSizeArray = ['100%', 380];
export const TAB_IDS = {
CHART: 'chart',
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
index cf547a49cac4c..2dece82e6f5c7 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
@@ -11,25 +11,35 @@ import { i18n } from '@kbn/i18n';
import moment from 'moment';
import {
EuiButtonEmpty,
+ EuiCheckbox,
EuiDatePicker,
EuiFlexGroup,
EuiFlexItem,
+ EuiIcon,
+ EuiIconTip,
EuiLoadingChart,
EuiModal,
EuiModalHeader,
EuiModalBody,
- EuiSelect,
EuiSpacer,
EuiTabs,
EuiTab,
+ EuiText,
+ EuiTitle,
EuiToolTip,
+ htmlIdGenerator,
} from '@elastic/eui';
import {
+ AnnotationDomainType,
Axis,
Chart,
CurveType,
+ LineAnnotation,
LineSeries,
+ LineAnnotationDatum,
Position,
+ RectAnnotation,
+ RectAnnotationDatum,
ScaleType,
Settings,
timeFormatter,
@@ -42,7 +52,6 @@ import { useMlApiContext } from '../../../../contexts/kibana';
import { useCurrentEuiTheme } from '../../../../components/color_range_legend';
import { JobMessagesPane } from '../job_details/job_messages_pane';
import { EditQueryDelay } from './edit_query_delay';
-import { getIntervalOptions } from './get_interval_options';
import {
CHART_DIRECTION,
ChartDirectionType,
@@ -53,12 +62,18 @@ import {
} from './constants';
import { loadFullJob } from '../utils';
-const dateFormatter = timeFormatter('MM-DD HH:mm');
+const dateFormatter = timeFormatter('MM-DD HH:mm:ss');
+const MAX_CHART_POINTS = 480;
interface DatafeedModalProps {
jobId: string;
end: number;
- onClose: (deletionApproved?: boolean) => void;
+ onClose: () => void;
+}
+
+function setLineAnnotationHeader(lineDatum: LineAnnotationDatum) {
+ lineDatum.header = dateFormatter(lineDatum.dataValue);
+ return lineDatum;
}
export const DatafeedModal: FC = ({ jobId, end, onClose }) => {
@@ -68,11 +83,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
isInitialized: boolean;
}>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false });
const [endDate, setEndDate] = useState(moment(end));
- const [interval, setInterval] = useState();
const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.CHART);
const [isLoadingChartData, setIsLoadingChartData] = useState(false);
const [bucketData, setBucketData] = useState([]);
+ const [annotationData, setAnnotationData] = useState<{
+ rect: RectAnnotationDatum[];
+ line: LineAnnotationDatum[];
+ }>({ rect: [], line: [] });
+ const [modelSnapshotData, setModelSnapshotData] = useState([]);
const [sourceData, setSourceData] = useState([]);
+ const [showAnnotations, setShowAnnotations] = useState(true);
+ const [showModelSnapshots, setShowModelSnapshots] = useState(true);
const {
results: { getDatafeedResultChartData },
@@ -102,25 +123,30 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
const handleChange = (date: moment.Moment) => setEndDate(date);
const handleEndDateChange = (direction: ChartDirectionType) => {
- if (interval === undefined) return;
+ if (data.bucketSpan === undefined) return;
const newEndDate = endDate.clone();
- const [count, type] = interval.split(' ');
+ const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
+ const unit = unitMatch[0];
+ const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
if (direction === CHART_DIRECTION.FORWARD) {
- newEndDate.add(Number(count), type);
+ newEndDate.add(MAX_CHART_POINTS * count, unit);
} else {
- newEndDate.subtract(Number(count), type);
+ newEndDate.subtract(MAX_CHART_POINTS * count, unit);
}
setEndDate(newEndDate);
};
const getChartData = useCallback(async () => {
- if (interval === undefined) return;
+ if (data.bucketSpan === undefined) return;
const endTimestamp = moment(endDate).valueOf();
- const [count, type] = interval.split(' ');
- const startMoment = endDate.clone().subtract(Number(count), type);
+ const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
+ const unit = unitMatch[0];
+ const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
+ // STARTTIME = ENDTIME - (BucketSpan * MAX_CHART_POINTS)
+ const startMoment = endDate.clone().subtract(MAX_CHART_POINTS * count, unit);
const startTimestamp = moment(startMoment).valueOf();
try {
@@ -128,6 +154,11 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
setSourceData(chartData.datafeedResults);
setBucketData(chartData.bucketResults);
+ setAnnotationData({
+ rect: chartData.annotationResultsRect,
+ line: chartData.annotationResultsLine.map(setLineAnnotationHeader),
+ });
+ setModelSnapshotData(chartData.modelSnapshotResultsLine.map(setLineAnnotationHeader));
} catch (error) {
const title = i18n.translate('xpack.ml.jobsList.datafeedModal.errorToastTitle', {
defaultMessage: 'Error fetching data',
@@ -135,7 +166,7 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
displayErrorToast(error, title);
}
setIsLoadingChartData(false);
- }, [endDate, interval]);
+ }, [endDate, data.bucketSpan]);
const getJobData = async () => {
try {
@@ -145,11 +176,6 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
bucketSpan: job.analysis_config.bucket_span,
isInitialized: true,
});
- const intervalOptions = getIntervalOptions(job.analysis_config.bucket_span);
- const initialInterval = intervalOptions.length
- ? intervalOptions[intervalOptions.length - 1]
- : undefined;
- setInterval(initialInterval?.value || '72 hours');
} catch (error) {
displayErrorToast(error);
}
@@ -161,20 +187,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
useEffect(
function loadChartData() {
- if (interval !== undefined) {
+ if (data.bucketSpan !== undefined) {
setIsLoadingChartData(true);
getChartData();
}
},
- [endDate, interval]
+ [endDate, data.bucketSpan]
);
const { datafeedConfig, bucketSpan, isInitialized } = data;
-
- const intervalOptions = useMemo(() => {
- if (bucketSpan === undefined) return [];
- return getIntervalOptions(bucketSpan);
- }, [bucketSpan]);
+ const checkboxIdAnnotation = useMemo(() => htmlIdGenerator()(), []);
+ const checkboxIdModelSnapshot = useMemo(() => htmlIdGenerator()(), []);
return (
= ({ jobId, end, onClose }) =
-
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
= ({ jobId, end, onClose }) =
-
- setInterval(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.ml.jobsList.datafeedModal.intervalSelection',
- {
- defaultMessage: 'Datafeed modal chart interval selection',
- }
- )}
- />
-
= ({ jobId, end, onClose }) =
isEnabled={datafeedConfig.state === DATAFEED_STATE.STOPPED}
/>
+
+
+
+
+
+
+ }
+ checked={showAnnotations}
+ onChange={() => setShowAnnotations(!showAnnotations)}
+ />
+
+
+
+
+
+ }
+ checked={showModelSnapshots}
+ onChange={() => setShowModelSnapshots(!showModelSnapshots)}
+ />
+
+
+
@@ -298,7 +362,65 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
})}
position={Position.Left}
/>
+ {showModelSnapshots ? (
+ }
+ markerPosition={Position.Top}
+ style={{
+ line: {
+ strokeWidth: 3,
+ stroke: euiTheme.euiColorVis1,
+ opacity: 0.5,
+ },
+ }}
+ />
+ ) : null}
+ {showAnnotations ? (
+ <>
+ }
+ markerPosition={Position.Top}
+ style={{
+ line: {
+ strokeWidth: 3,
+ stroke: euiTheme.euiColorDangerText,
+ opacity: 0.5,
+ },
+ }}
+ />
+
+ >
+ ) : null}
= ({ jobId, end, onClose }) =
curve={CurveType.LINEAR}
/>
{
- const unitMatch = bucketSpan.match(/[d | h| m | s]/g)!;
- const unit = unitMatch[0];
- const count = Number(bucketSpan.replace(/[^0-9]/g, ''));
-
- const intervalOptions = [];
-
- if (['s', 'ms', 'micros', 'nanos'].includes(unit)) {
- intervalOptions.push(
- {
- value: '1 hour',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.1hourOption', {
- defaultMessage: '{count} hour',
- values: { count: 1 },
- }),
- },
- {
- value: '2 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.2hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 2 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count <= 4) || unit === 'h') {
- intervalOptions.push(
- {
- value: '3 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.3hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 3 },
- }),
- },
- {
- value: '8 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.8hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 8 },
- }),
- },
- {
- value: '12 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.12hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 12 },
- }),
- },
- {
- value: '24 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.24hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 24 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count >= 5 && count <= 15) || unit === 'h') {
- intervalOptions.push(
- {
- value: '48 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.48hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 48 },
- }),
- },
- {
- value: '72 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.72hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 72 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count >= 10 && count <= 15) || unit === 'h' || unit === 'd') {
- intervalOptions.push(
- {
- value: '5 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.5daysOption', {
- defaultMessage: '{count} days',
- values: { count: 5 },
- }),
- },
- {
- value: '7 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.7daysOption', {
- defaultMessage: '{count} days',
- values: { count: 7 },
- }),
- }
- );
- }
-
- if (unit === 'h' || unit === 'd') {
- intervalOptions.push({
- value: '14 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.14DaysOption', {
- defaultMessage: '{count} days',
- values: { count: 14 },
- }),
- });
- }
-
- return intervalOptions;
-};
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
index b514c8433daf4..d3856e6afa398 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
@@ -7,26 +7,29 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
-
-import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { extractJobDetails } from './extract_job_details';
import { JsonPane } from './json_tab';
import { DatafeedPreviewPane } from './datafeed_preview_tab';
import { AnnotationsTable } from '../../../../components/annotations/annotations_table';
+import { DatafeedModal } from '../datafeed_modal';
import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout';
import { ModelSnapshotTable } from '../../../../components/model_snapshots';
import { ForecastsTable } from './forecasts_table';
import { JobDetailsPane } from './job_details_pane';
import { JobMessagesPane } from './job_messages_pane';
-import { i18n } from '@kbn/i18n';
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
export class JobDetailsUI extends Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ datafeedModalVisible: false,
+ };
if (this.props.addYourself) {
this.props.addYourself(props.jobId, (j) => this.updateJob(j));
}
@@ -77,6 +80,30 @@ export class JobDetailsUI extends Component {
alertRules,
} = extractJobDetails(job, basePath, refreshJobList);
+ datafeed.titleAction = (
+
+ }
+ >
+
+ this.setState({
+ datafeedModalVisible: true,
+ })
+ }
+ />
+
+ );
+
const tabs = [
{
id: 'job-settings',
@@ -105,6 +132,32 @@ export class JobDetailsUI extends Component {
/>
),
},
+ {
+ id: 'datafeed',
+ 'data-test-subj': 'mlJobListTab-datafeed',
+ name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
+ defaultMessage: 'Datafeed',
+ }),
+ content: (
+ <>
+
+ {this.props.jobId && this.state.datafeedModalVisible ? (
+ {
+ this.setState({
+ datafeedModalVisible: false,
+ });
+ }}
+ end={job.data_counts.latest_bucket_timestamp}
+ jobId={this.props.jobId}
+ />
+ ) : null}
+ >
+ ),
+ },
{
id: 'counts',
'data-test-subj': 'mlJobListTab-counts',
@@ -137,21 +190,6 @@ export class JobDetailsUI extends Component {
];
if (showFullDetails && datafeed.items.length) {
- // Datafeed should be at index 2 in tabs array for full details
- tabs.splice(2, 0, {
- id: 'datafeed',
- 'data-test-subj': 'mlJobListTab-datafeed',
- name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
- defaultMessage: 'Datafeed',
- }),
- content: (
-
- ),
- });
-
tabs.push(
{
id: 'datafeed-preview',
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
index 49d9bcde49052..4046f4d5d8071 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
@@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
+ EuiFlexGroup,
+ EuiFlexItem,
EuiTitle,
EuiTable,
EuiTableBody,
@@ -42,9 +44,14 @@ function Section({ section }) {
return (
-
- {section.title}
-
+
+
+
+ {section.title}
+
+
+ {section.titleAction}
+
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
index 19ba5aa304bf0..25ef36782207f 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
@@ -6,7 +6,10 @@
*/
// Service for obtaining data for the ML Results dashboards.
-import { GetStoppedPartitionResult } from '../../../../common/types/results';
+import {
+ GetStoppedPartitionResult,
+ GetDatafeedResultsChartDataResult,
+} from '../../../../common/types/results';
import { HttpService } from '../http_service';
import { basePath } from './index';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
@@ -148,7 +151,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({
start,
end,
});
- return httpService.http({
+ return httpService.http({
path: `${basePath()}/results/datafeed_results_chart`,
method: 'POST',
body,
diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts
index e3a4a8348ebc1..917619a67fea9 100644
--- a/x-pack/plugins/ml/public/plugin.ts
+++ b/x-pack/plugins/ml/public/plugin.ts
@@ -44,7 +44,6 @@ import { registerFeature } from './register_feature';
// Not importing from `ml_url_generator/index` here to avoid importing unnecessary code
import { registerUrlGenerator } from './ml_url_generator/ml_url_generator';
import type { MapsStartApi } from '../../maps/public';
-import { LensPublicStart } from '../../lens/public';
import {
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
@@ -62,7 +61,6 @@ export interface MlStartDependencies {
spaces?: SpacesPluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
- lens?: LensPublicStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
dataVisualizer: DataVisualizerPluginStart;
}
@@ -119,7 +117,6 @@ export class MlPlugin implements Plugin {
embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable },
maps: pluginsStart.maps,
uiActions: pluginsStart.uiActions,
- lens: pluginsStart.lens,
kibanaVersion,
triggersActionsUi: pluginsStart.triggersActionsUi,
dataVisualizer: pluginsStart.dataVisualizer,
diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts
index 9413ee00184d2..81ee394b99704 100644
--- a/x-pack/plugins/ml/server/models/results_service/results_service.ts
+++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts
@@ -27,6 +27,7 @@ import {
import { MlJobsResponse } from '../../../common/types/job_service';
import type { MlClient } from '../../lib/ml_client';
import { datafeedsProvider } from '../job_service/datafeeds';
+import { annotationServiceProvider } from '../annotation_service';
// Service for carrying out Elasticsearch queries to obtain data for the
// ML Results dashboards.
@@ -620,13 +621,19 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
const finalResults: GetDatafeedResultsChartDataResult = {
bucketResults: [],
datafeedResults: [],
+ annotationResultsRect: [],
+ annotationResultsLine: [],
+ modelSnapshotResultsLine: [],
};
const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient);
- const datafeedConfig = await getDatafeedByJobId(jobId);
- const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId });
- if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) {
+ const [datafeedConfig, { body: jobsResponse }] = await Promise.all([
+ getDatafeedByJobId(jobId),
+ mlClient.getJobs({ job_id: jobId }),
+ ]);
+
+ if (jobsResponse && (jobsResponse.count === 0 || jobsResponse.jobs === undefined)) {
throw Boom.notFound(`Job with the id "${jobId}" not found`);
}
@@ -696,10 +703,25 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
]) || [];
}
- const bucketResp = await mlClient.getBuckets({
- job_id: jobId,
- body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
- });
+ const { getAnnotations } = annotationServiceProvider(client!);
+
+ const [bucketResp, annotationResp, { body: modelSnapshotsResp }] = await Promise.all([
+ mlClient.getBuckets({
+ job_id: jobId,
+ body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
+ }),
+ getAnnotations({
+ jobIds: [jobId],
+ earliestMs: start,
+ latestMs: end,
+ maxAnnotations: 1000,
+ }),
+ mlClient.getModelSnapshots({
+ job_id: jobId,
+ start: String(start),
+ end: String(end),
+ }),
+ ]);
const bucketResults = bucketResp?.body?.buckets ?? [];
bucketResults.forEach((dataForTime) => {
@@ -708,6 +730,36 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
finalResults.bucketResults.push([timestamp, eventCount]);
});
+ const annotationResults = annotationResp.annotations[jobId] || [];
+ annotationResults.forEach((annotation) => {
+ const timestamp = Number(annotation?.timestamp);
+ const endTimestamp = Number(annotation?.end_timestamp);
+ if (timestamp === endTimestamp) {
+ finalResults.annotationResultsLine.push({
+ dataValue: timestamp,
+ details: annotation.annotation,
+ });
+ } else {
+ finalResults.annotationResultsRect.push({
+ coordinates: {
+ x0: timestamp,
+ x1: endTimestamp,
+ },
+ details: annotation.annotation,
+ });
+ }
+ });
+
+ const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? [];
+ modelSnapshots.forEach((modelSnapshot) => {
+ const timestamp = Number(modelSnapshot?.timestamp);
+
+ finalResults.modelSnapshotResultsLine.push({
+ dataValue: timestamp,
+ details: modelSnapshot.description,
+ });
+ });
+
return finalResults;
}
diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json
index 221718d423383..8e859c35e3f85 100644
--- a/x-pack/plugins/ml/tsconfig.json
+++ b/x-pack/plugins/ml/tsconfig.json
@@ -16,7 +16,7 @@
"../../../typings/**/*",
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
"public/**/*.json",
- "server/**/*.json",
+ "server/**/*.json"
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
@@ -28,7 +28,6 @@
{ "path": "../license_management/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../maps/tsconfig.json" },
- { "path": "../lens/tsconfig.json" },
{ "path": "../security/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
{ "path": "../alerting/tsconfig.json" },
diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx
index 8b4075ba67cdc..44af8b3327975 100644
--- a/x-pack/plugins/monitoring/public/alerts/badge.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx
@@ -19,13 +19,18 @@ import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category';
import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node';
export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`;
+export const numberOfRulesLabel = (count: number) => `${count} rule${count > 1 ? 's' : ''}`;
const MAX_TO_SHOW_BY_CATEGORY = 8;
-const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
+const PANEL_TITLE_ALERTS = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
});
+const PANEL_TITLE_RULES = i18n.translate('xpack.monitoring.rules.badge.panelTitle', {
+ defaultMessage: 'Rules',
+});
+
const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', {
defaultMessage: 'Group by node',
});
@@ -54,6 +59,7 @@ export const AlertsBadge: React.FC = (props: Props) => {
const [showByNode, setShowByNode] = React.useState(
!inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY
);
+ const PANEL_TITLE = inSetupMode ? PANEL_TITLE_RULES : PANEL_TITLE_ALERTS;
React.useEffect(() => {
if (inSetupMode && showByNode) {
@@ -93,10 +99,12 @@ export const AlertsBadge: React.FC = (props: Props) => {
setShowPopover(true)}
>
- {numberOfAlertsLabel(alertCount)}
+ {inSetupMode ? numberOfRulesLabel(alertCount) : numberOfAlertsLabel(alertCount)}
);
diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap
index 5018bad317708..e3fa9da6639b3 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap
@@ -22,33 +22,37 @@ Array [
-
- There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting.
-
-
-
+ There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting.
+
+
- 403 Forbidden
-
-
- no access for you
-
-
- 500 Internal Server Error
-
-
- An internal server error occurred
-
-
+
+ 403 Forbidden
+
+
+ no access for you
+
+
+ 500 Internal Server Error
+
+
+ An internal server error occurred
+
+
+
,
]
diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
index fe277062bc95a..34a4c049dddcc 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
@@ -9,7 +9,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
>
No monitoring data found.
-
@@ -87,7 +87,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
-
+
`;
@@ -100,7 +100,7 @@ exports[`NoData should show text next to the spinner while checking a setting 1`
>
No monitoring data found.
-
@@ -178,6 +178,6 @@ exports[`NoData should show text next to the spinner while checking a setting 1`
-
+
`;
diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
index 7f38a92beae8f..7b04e6410d996 100644
--- a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
@@ -5,7 +5,7 @@ exports[`PageLoading should show a simple page loading component 1`] = `
class="euiPage euiPage--paddingMedium euiPage--grow"
style="height:calc(100vh - 50px)"
>
-
-
+
`;
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx
index c358067123747..f92f12c79a56d 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx
@@ -49,7 +49,7 @@ describe('CreateCaseFlyout', () => {
);
- wrapper.find('.euiFlyout__closeButton').first().simulate('click');
+ wrapper.find(`[data-test-subj='euiFlyoutCloseButton']`).first().simulate('click');
expect(onCloseFlyout).toBeCalled();
});
});
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx
index df29d02e8d830..b6cdcf3111672 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx
@@ -5,8 +5,8 @@
* 2.0.
*/
-import React, { memo } from 'react';
-import styled from 'styled-components';
+import React, { memo, ReactNode } from 'react';
+import styled, { StyledComponent } from 'styled-components';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import * as i18n from '../translations';
@@ -20,7 +20,11 @@ export interface CreateCaseModalProps {
onSuccess: (theCase: Case) => Promise;
}
-const StyledFlyout = styled(EuiFlyout)`
+// TODO: EUI team follow up on complex types and styled-components `styled`
+// https://github.com/elastic/eui/issues/4855
+const StyledFlyout: StyledComponent = styled(
+ EuiFlyout
+)`
${({ theme }) => `
z-index: ${theme.eui.euiZModal};
`}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
index ea69a371cedae..3566835b1701c 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx
@@ -38,6 +38,12 @@ export function EmptyView({
emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT;
}
+ if (!series) {
+ emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
+ defaultMessage: 'No series found. Please add a series.',
+ });
+ }
+
return (
{loading && (
@@ -77,7 +83,7 @@ export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBui
export const CHOOSE_REPORT_DEFINITION = i18n.translate(
'xpack.observability.expView.seriesBuilder.emptyReportDefinition',
{
- defaultMessage: 'Select a report type to create a visualization.',
+ defaultMessage: 'Select a report definition to create a visualization.',
}
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
index af64e74bca89c..fe2953edd36d6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx
@@ -29,6 +29,7 @@ describe('FilterLabel', function () {
negate={false}
seriesId={'kpi-over-time'}
removeFilter={jest.fn()}
+ indexPattern={mockIndexPattern}
/>
);
@@ -52,6 +53,7 @@ describe('FilterLabel', function () {
negate={false}
seriesId={'kpi-over-time'}
removeFilter={removeFilter}
+ indexPattern={mockIndexPattern}
/>
);
@@ -74,6 +76,7 @@ describe('FilterLabel', function () {
negate={false}
seriesId={'kpi-over-time'}
removeFilter={removeFilter}
+ indexPattern={mockIndexPattern}
/>
);
@@ -99,6 +102,7 @@ describe('FilterLabel', function () {
negate={true}
seriesId={'kpi-over-time'}
removeFilter={jest.fn()}
+ indexPattern={mockIndexPattern}
/>
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
index 3d4ba6dc08c37..a08e777c5ea71 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
+import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { useSeriesFilters } from '../hooks/use_series_filters';
import { FilterValueLabel } from '../../filter_value_label/filter_value_label';
@@ -17,6 +17,7 @@ interface Props {
seriesId: string;
negate: boolean;
definitionFilter?: boolean;
+ indexPattern: IndexPattern;
removeFilter: (field: string, value: string, notVal: boolean) => void;
}
@@ -26,11 +27,10 @@ export function FilterLabel({
field,
value,
negate,
+ indexPattern,
removeFilter,
definitionFilter,
}: Props) {
- const { indexPattern } = useAppIndexPatternContext();
-
const { invertFilter } = useSeriesFilters({ seriesId });
return indexPattern ? (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
index e119507860c5c..01e8d023ae96b 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts
@@ -5,8 +5,15 @@
* 2.0.
*/
-import { ReportViewTypeId } from '../../types';
-import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames';
+import { ReportViewType } from '../../types';
+import {
+ CLS_FIELD,
+ FCP_FIELD,
+ FID_FIELD,
+ LCP_FIELD,
+ TBT_FIELD,
+ TRANSACTION_TIME_TO_FIRST_BYTE,
+} from './elasticsearch_fieldnames';
import {
AGENT_HOST_LABEL,
BROWSER_FAMILY_LABEL,
@@ -58,6 +65,7 @@ export const FieldLabels: Record = {
[TBT_FIELD]: TBT_LABEL,
[FID_FIELD]: FID_LABEL,
[CLS_FIELD]: CLS_LABEL,
+ [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time',
'monitor.id': MONITOR_ID_LABEL,
'monitor.status': MONITOR_STATUS_LABEL,
@@ -77,11 +85,11 @@ export const FieldLabels: Record = {
'http.request.method': REQUEST_METHOD,
};
-export const DataViewLabels: Record = {
- dist: PERF_DIST_LABEL,
- kpi: KPI_OVER_TIME_LABEL,
- cwv: CORE_WEB_VITALS_LABEL,
- mdd: DEVICE_DISTRIBUTION_LABEL,
+export const DataViewLabels: Record = {
+ 'data-distribution': PERF_DIST_LABEL,
+ 'kpi-over-time': KPI_OVER_TIME_LABEL,
+ 'core-web-vitals': CORE_WEB_VITALS_LABEL,
+ 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL,
};
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
index 07342d976cbea..574a9f6a2bc10 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { AppDataType, ReportViewTypes } from '../types';
+import { AppDataType, ReportViewType } from '../types';
import { getRumDistributionConfig } from './rum/data_distribution_config';
import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config';
import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config';
@@ -17,7 +17,7 @@ import { getMobileKPIDistributionConfig } from './mobile/distribution_config';
import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config';
interface Props {
- reportType: keyof typeof ReportViewTypes;
+ reportType: ReportViewType;
indexPattern: IndexPattern;
dataType: AppDataType;
}
@@ -25,23 +25,23 @@ interface Props {
export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => {
switch (dataType) {
case 'ux':
- if (reportType === 'dist') {
+ if (reportType === 'data-distribution') {
return getRumDistributionConfig({ indexPattern });
}
- if (reportType === 'cwv') {
+ if (reportType === 'core-web-vitals') {
return getCoreWebVitalsConfig({ indexPattern });
}
return getKPITrendsLensConfig({ indexPattern });
case 'synthetics':
- if (reportType === 'dist') {
+ if (reportType === 'data-distribution') {
return getSyntheticsDistributionConfig({ indexPattern });
}
return getSyntheticsKPIConfig({ indexPattern });
case 'mobile':
- if (reportType === 'dist') {
+ if (reportType === 'data-distribution') {
return getMobileKPIDistributionConfig({ indexPattern });
}
- if (reportType === 'mdd') {
+ if (reportType === 'device-data-distribution') {
return getMobileDeviceDistributionConfig({ indexPattern });
}
return getMobileKPIConfig({ indexPattern });
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
index 8b21df64a3c91..5189a529bda8f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts
@@ -5,25 +5,37 @@
* 2.0.
*/
-import { LensAttributes } from './lens_attributes';
+import { LayerConfig, LensAttributes } from './lens_attributes';
import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers';
import { getDefaultConfigs } from './default_configs';
import { sampleAttribute } from './test_data/sample_attribute';
-import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames';
+import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames';
+import { buildExistsFilter, buildPhrasesFilter } from './utils';
describe('Lens Attribute', () => {
mockAppIndexPattern();
const reportViewConfig = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
dataType: 'ux',
indexPattern: mockIndexPattern,
});
+ reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern));
+
let lnsAttr: LensAttributes;
+ const layerConfig: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: {},
+ time: { from: 'now-15m', to: 'now' },
+ };
+
beforeEach(() => {
- lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {});
+ lnsAttr = new LensAttributes([layerConfig]);
});
it('should return expected json', function () {
@@ -31,7 +43,7 @@ describe('Lens Attribute', () => {
});
it('should return main y axis', function () {
- expect(lnsAttr.getMainYAxis()).toEqual({
+ expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({
dataType: 'number',
isBucketed: false,
label: 'Pages loaded',
@@ -42,7 +54,7 @@ describe('Lens Attribute', () => {
});
it('should return expected field type', function () {
- expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual(
+ expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
@@ -60,7 +72,7 @@ describe('Lens Attribute', () => {
});
it('should return expected field type for custom field with default value', function () {
- expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
+ expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
@@ -79,11 +91,18 @@ describe('Lens Attribute', () => {
});
it('should return expected field type for custom field with passed value', function () {
- lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {
- 'performance.metric': [LCP_FIELD],
- });
+ const layerConfig1: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: { 'performance.metric': [LCP_FIELD] },
+ time: { from: 'now-15m', to: 'now' },
+ };
- expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
+ lnsAttr = new LensAttributes([layerConfig1]);
+
+ expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
@@ -102,7 +121,7 @@ describe('Lens Attribute', () => {
});
it('should return expected number range column', function () {
- expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
+ expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
@@ -124,7 +143,7 @@ describe('Lens Attribute', () => {
});
it('should return expected number operation column', function () {
- expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
+ expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
@@ -160,7 +179,7 @@ describe('Lens Attribute', () => {
});
it('should return main x axis', function () {
- expect(lnsAttr.getXAxis()).toEqual({
+ expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
@@ -182,38 +201,45 @@ describe('Lens Attribute', () => {
});
it('should return first layer', function () {
- expect(lnsAttr.getLayer()).toEqual({
- columnOrder: ['x-axis-column', 'y-axis-column'],
- columns: {
- 'x-axis-column': {
- dataType: 'number',
- isBucketed: true,
- label: 'Page load time',
- operationType: 'range',
- params: {
- maxBars: 'auto',
- ranges: [
- {
- from: 0,
- label: '',
- to: 1000,
- },
- ],
- type: 'histogram',
+ expect(lnsAttr.getLayers()).toEqual({
+ layer0: {
+ columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
+ columns: {
+ 'x-axis-column-layer0': {
+ dataType: 'number',
+ isBucketed: true,
+ label: 'Page load time',
+ operationType: 'range',
+ params: {
+ maxBars: 'auto',
+ ranges: [
+ {
+ from: 0,
+ label: '',
+ to: 1000,
+ },
+ ],
+ type: 'histogram',
+ },
+ scale: 'interval',
+ sourceField: 'transaction.duration.us',
+ },
+ 'y-axis-column-layer0': {
+ dataType: 'number',
+ isBucketed: false,
+ label: 'Pages loaded',
+ operationType: 'count',
+ scale: 'ratio',
+ sourceField: 'Records',
+ filter: {
+ language: 'kuery',
+ query:
+ 'transaction.type: page-load and processor.event: transaction and transaction.type : *',
+ },
},
- scale: 'interval',
- sourceField: 'transaction.duration.us',
- },
- 'y-axis-column': {
- dataType: 'number',
- isBucketed: false,
- label: 'Pages loaded',
- operationType: 'count',
- scale: 'ratio',
- sourceField: 'Records',
},
+ incompleteColumns: {},
},
- incompleteColumns: {},
});
});
@@ -225,12 +251,12 @@ describe('Lens Attribute', () => {
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
layers: [
{
- accessors: ['y-axis-column'],
- layerId: 'layer1',
+ accessors: ['y-axis-column-layer0'],
+ layerId: 'layer0',
palette: undefined,
seriesType: 'line',
- xAccessor: 'x-axis-column',
- yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
+ xAccessor: 'x-axis-column-layer0',
+ yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
},
],
legend: { isVisible: true, position: 'right' },
@@ -240,108 +266,52 @@ describe('Lens Attribute', () => {
});
});
- describe('ParseFilters function', function () {
- it('should parse default filters', function () {
- expect(lnsAttr.parseFilters()).toEqual([
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
- ]);
- });
-
- it('should parse default and ui filters', function () {
- lnsAttr = new LensAttributes(
- mockIndexPattern,
- reportViewConfig,
- 'line',
- [
- { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] },
- { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] },
- ],
- 'count',
- {}
- );
+ describe('Layer breakdowns', function () {
+ it('should return breakdown column', function () {
+ const layerConfig1: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: { 'performance.metric': [LCP_FIELD] },
+ breakdown: USER_AGENT_NAME,
+ time: { from: 'now-15m', to: 'now' },
+ };
- expect(lnsAttr.parseFilters()).toEqual([
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
- {
- meta: {
- index: 'apm-*',
- key: 'service.name',
- params: ['elastic-co', 'kibana-front'],
- type: 'phrases',
- value: 'elastic-co, kibana-front',
- },
- query: {
- bool: {
- minimum_should_match: 1,
- should: [
- {
- match_phrase: {
- 'service.name': 'elastic-co',
- },
- },
- {
- match_phrase: {
- 'service.name': 'kibana-front',
- },
- },
- ],
- },
- },
- },
- {
- meta: {
- index: 'apm-*',
- },
- query: {
- match_phrase: {
- 'user_agent.name': 'Firefox',
- },
- },
- },
- {
- meta: {
- index: 'apm-*',
- negate: true,
- },
- query: {
- match_phrase: {
- 'user_agent.name': 'Chrome',
- },
- },
- },
- ]);
- });
- });
+ lnsAttr = new LensAttributes([layerConfig1]);
- describe('Layer breakdowns', function () {
- it('should add breakdown column', function () {
- lnsAttr.addBreakdown(USER_AGENT_NAME);
+ lnsAttr.getBreakdownColumn({
+ sourceField: USER_AGENT_NAME,
+ layerId: 'layer0',
+ indexPattern: mockIndexPattern,
+ });
expect(lnsAttr.visualization.layers).toEqual([
{
- accessors: ['y-axis-column'],
- layerId: 'layer1',
+ accessors: ['y-axis-column-layer0'],
+ layerId: 'layer0',
palette: undefined,
seriesType: 'line',
- splitAccessor: 'break-down-column',
- xAccessor: 'x-axis-column',
- yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
+ splitAccessor: 'breakdown-column-layer0',
+ xAccessor: 'x-axis-column-layer0',
+ yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
},
]);
- expect(lnsAttr.layers.layer1).toEqual({
- columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'],
+ expect(lnsAttr.layers.layer0).toEqual({
+ columnOrder: ['x-axis-column-layer0', 'breakdown-column-layer0', 'y-axis-column-layer0'],
columns: {
- 'break-down-column': {
+ 'breakdown-column-layer0': {
dataType: 'string',
isBucketed: true,
label: 'Top values of Browser family',
operationType: 'terms',
params: {
missingBucket: false,
- orderBy: { columnId: 'y-axis-column', type: 'column' },
+ orderBy: {
+ columnId: 'y-axis-column-layer0',
+ type: 'column',
+ },
orderDirection: 'desc',
otherBucket: true,
size: 10,
@@ -349,10 +319,10 @@ describe('Lens Attribute', () => {
scale: 'ordinal',
sourceField: 'user_agent.name',
},
- 'x-axis-column': {
+ 'x-axis-column-layer0': {
dataType: 'number',
isBucketed: true,
- label: 'Page load time',
+ label: 'Largest contentful paint',
operationType: 'range',
params: {
maxBars: 'auto',
@@ -360,62 +330,47 @@ describe('Lens Attribute', () => {
type: 'histogram',
},
scale: 'interval',
- sourceField: 'transaction.duration.us',
+ sourceField: 'transaction.marks.agent.largestContentfulPaint',
},
- 'y-axis-column': {
+ 'y-axis-column-layer0': {
dataType: 'number',
isBucketed: false,
label: 'Pages loaded',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
+ filter: {
+ language: 'kuery',
+ query:
+ 'transaction.type: page-load and processor.event: transaction and transaction.type : *',
+ },
},
},
incompleteColumns: {},
});
});
+ });
- it('should remove breakdown column', function () {
- lnsAttr.addBreakdown(USER_AGENT_NAME);
-
- lnsAttr.removeBreakdown();
+ describe('Layer Filters', function () {
+ it('should return expected filters', function () {
+ reportViewConfig.filters?.push(
+ ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern)
+ );
- expect(lnsAttr.visualization.layers).toEqual([
- {
- accessors: ['y-axis-column'],
- layerId: 'layer1',
- palette: undefined,
- seriesType: 'line',
- xAccessor: 'x-axis-column',
- yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
- },
- ]);
+ const layerConfig1: LayerConfig = {
+ reportConfig: reportViewConfig,
+ seriesType: 'line',
+ operationType: 'count',
+ indexPattern: mockIndexPattern,
+ reportDefinitions: { 'performance.metric': [LCP_FIELD] },
+ time: { from: 'now-15m', to: 'now' },
+ };
- expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']);
+ const filters = lnsAttr.getLayerFilters(layerConfig1, 2);
- expect(lnsAttr.layers.layer1.columns).toEqual({
- 'x-axis-column': {
- dataType: 'number',
- isBucketed: true,
- label: 'Page load time',
- operationType: 'range',
- params: {
- maxBars: 'auto',
- ranges: [{ from: 0, label: '', to: 1000 }],
- type: 'histogram',
- },
- scale: 'interval',
- sourceField: 'transaction.duration.us',
- },
- 'y-axis-column': {
- dataType: 'number',
- isBucketed: false,
- label: 'Pages loaded',
- operationType: 'count',
- scale: 'ratio',
- sourceField: 'Records',
- },
- });
+ expect(filters).toEqual(
+ '@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)'
+ );
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
index 22ad18c663b32..208e8d8ba43c2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts
@@ -27,13 +27,12 @@ import {
TermsIndexPatternColumn,
CardinalityIndexPatternColumn,
} from '../../../../../../lens/public';
-import {
- buildPhraseFilter,
- buildPhrasesFilter,
- IndexPattern,
-} from '../../../../../../../../src/plugins/data/common';
+import { urlFiltersToKueryString } from '../utils/stringify_kueries';
+import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants';
import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types';
+import { PersistableFilter } from '../../../../../../lens/common';
+import { parseAbsoluteDate } from '../series_date_picker/date_range_picker';
function getLayerReferenceName(layerId: string) {
return `indexpattern-datasource-layer-${layerId}`;
@@ -87,46 +86,50 @@ export const parseCustomFieldName = (
return { fieldName, columnType, columnFilters, timeScale, columnLabel };
};
-export class LensAttributes {
+export interface LayerConfig {
+ filters?: UrlFilter[];
+ reportConfig: DataSeries;
+ breakdown?: string;
+ seriesType?: SeriesType;
+ operationType?: OperationType;
+ reportDefinitions: URLReportDefinition;
+ time: { to: string; from: string };
indexPattern: IndexPattern;
+}
+
+export class LensAttributes {
layers: Record;
visualization: XYState;
- filters: UrlFilter[];
- seriesType: SeriesType;
- reportViewConfig: DataSeries;
- reportDefinitions: URLReportDefinition;
- breakdownSource?: string;
+ layerConfigs: LayerConfig[];
- constructor(
- indexPattern: IndexPattern,
- reportViewConfig: DataSeries,
- seriesType?: SeriesType,
- filters?: UrlFilter[],
- operationType?: OperationType,
- reportDefinitions?: URLReportDefinition,
- breakdownSource?: string
- ) {
- this.indexPattern = indexPattern;
+ constructor(layerConfigs: LayerConfig[]) {
this.layers = {};
- this.filters = filters ?? [];
- this.reportDefinitions = reportDefinitions ?? {};
- this.breakdownSource = breakdownSource;
-
- if (operationType) {
- reportViewConfig.yAxisColumns.forEach((yAxisColumn) => {
- if (typeof yAxisColumn.operationType !== undefined) {
- yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType'];
- }
- });
- }
- this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType;
- this.reportViewConfig = reportViewConfig;
- this.layers.layer1 = this.getLayer();
+
+ layerConfigs.forEach(({ reportConfig, operationType }) => {
+ if (operationType) {
+ reportConfig.yAxisColumns.forEach((yAxisColumn) => {
+ if (typeof yAxisColumn.operationType !== undefined) {
+ yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType'];
+ }
+ });
+ }
+ });
+
+ this.layerConfigs = layerConfigs;
+ this.layers = this.getLayers();
this.visualization = this.getXyState();
}
- getBreakdownColumn(sourceField: string): TermsIndexPatternColumn {
- const fieldMeta = this.indexPattern.getFieldByName(sourceField);
+ getBreakdownColumn({
+ sourceField,
+ layerId,
+ indexPattern,
+ }: {
+ sourceField: string;
+ layerId: string;
+ indexPattern: IndexPattern;
+ }): TermsIndexPatternColumn {
+ const fieldMeta = indexPattern.getFieldByName(sourceField);
return {
sourceField,
@@ -136,8 +139,8 @@ export class LensAttributes {
scale: 'ordinal',
isBucketed: true,
params: {
+ orderBy: { type: 'column', columnId: `y-axis-column-${layerId}` },
size: 10,
- orderBy: { type: 'column', columnId: 'y-axis-column' },
orderDirection: 'desc',
otherBucket: true,
missingBucket: false,
@@ -145,36 +148,14 @@ export class LensAttributes {
};
}
- addBreakdown(sourceField: string) {
- const { xAxisColumn } = this.reportViewConfig;
- if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) {
- // do nothing since this will be used a x axis source
- return;
- }
- this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField);
-
- this.layers.layer1.columnOrder = [
- 'x-axis-column',
- 'break-down-column',
- 'y-axis-column',
- ...Object.keys(this.getChildYAxises()),
- ];
-
- this.visualization.layers[0].splitAccessor = 'break-down-column';
- }
-
- removeBreakdown() {
- delete this.layers.layer1.columns['break-down-column'];
-
- this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column'];
-
- this.visualization.layers[0].splitAccessor = undefined;
- }
-
- getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn {
+ getNumberRangeColumn(
+ sourceField: string,
+ reportViewConfig: DataSeries,
+ label?: string
+ ): RangeIndexPatternColumn {
return {
sourceField,
- label: this.reportViewConfig.labels[sourceField] ?? label,
+ label: reportViewConfig.labels[sourceField] ?? label,
dataType: 'number',
operationType: 'range',
isBucketed: true,
@@ -187,16 +168,36 @@ export class LensAttributes {
};
}
- getCardinalityColumn(sourceField: string, label?: string) {
- return this.getNumberOperationColumn(sourceField, 'unique_count', label);
+ getCardinalityColumn({
+ sourceField,
+ label,
+ reportViewConfig,
+ }: {
+ sourceField: string;
+ label?: string;
+ reportViewConfig: DataSeries;
+ }) {
+ return this.getNumberOperationColumn({
+ sourceField,
+ operationType: 'unique_count',
+ label,
+ reportViewConfig,
+ });
}
- getNumberColumn(
- sourceField: string,
- columnType?: string,
- operationType?: string,
- label?: string
- ) {
+ getNumberColumn({
+ reportViewConfig,
+ label,
+ sourceField,
+ columnType,
+ operationType,
+ }: {
+ sourceField: string;
+ columnType?: string;
+ operationType?: string;
+ label?: string;
+ reportViewConfig: DataSeries;
+ }) {
if (columnType === 'operation' || operationType) {
if (
operationType === 'median' ||
@@ -204,48 +205,58 @@ export class LensAttributes {
operationType === 'sum' ||
operationType === 'unique_count'
) {
- return this.getNumberOperationColumn(sourceField, operationType, label);
+ return this.getNumberOperationColumn({
+ sourceField,
+ operationType,
+ label,
+ reportViewConfig,
+ });
}
if (operationType?.includes('th')) {
- return this.getPercentileNumberColumn(sourceField, operationType);
+ return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!);
}
}
- return this.getNumberRangeColumn(sourceField, label);
+ return this.getNumberRangeColumn(sourceField, reportViewConfig!, label);
}
- getNumberOperationColumn(
- sourceField: string,
- operationType: 'average' | 'median' | 'sum' | 'unique_count',
- label?: string
- ):
+ getNumberOperationColumn({
+ sourceField,
+ label,
+ reportViewConfig,
+ operationType,
+ }: {
+ sourceField: string;
+ operationType: 'average' | 'median' | 'sum' | 'unique_count';
+ label?: string;
+ reportViewConfig: DataSeries;
+ }):
| AvgIndexPatternColumn
| MedianIndexPatternColumn
| SumIndexPatternColumn
| CardinalityIndexPatternColumn {
return {
...buildNumberColumn(sourceField),
- label:
- label ||
- i18n.translate('xpack.observability.expView.columns.operation.label', {
- defaultMessage: '{operationType} of {sourceField}',
- values: {
- sourceField: this.reportViewConfig.labels[sourceField],
- operationType: capitalize(operationType),
- },
- }),
+ label: i18n.translate('xpack.observability.expView.columns.operation.label', {
+ defaultMessage: '{operationType} of {sourceField}',
+ values: {
+ sourceField: label || reportViewConfig.labels[sourceField],
+ operationType: capitalize(operationType),
+ },
+ }),
operationType,
};
}
getPercentileNumberColumn(
sourceField: string,
- percentileValue: string
+ percentileValue: string,
+ reportViewConfig: DataSeries
): PercentileIndexPatternColumn {
return {
...buildNumberColumn(sourceField),
label: i18n.translate('xpack.observability.expView.columns.label', {
defaultMessage: '{percentileValue} percentile of {sourceField}',
- values: { sourceField: this.reportViewConfig.labels[sourceField], percentileValue },
+ values: { sourceField: reportViewConfig.labels[sourceField], percentileValue },
}),
operationType: 'percentile',
params: { percentile: Number(percentileValue.split('th')[0]) },
@@ -268,7 +279,7 @@ export class LensAttributes {
return {
operationType: 'terms',
sourceField,
- label: label || 'Top values of ' + sourceField,
+ label: 'Top values of ' + label || sourceField,
dataType: 'string',
isBucketed: true,
scale: 'ordinal',
@@ -283,30 +294,45 @@ export class LensAttributes {
};
}
- getXAxis() {
- const { xAxisColumn } = this.reportViewConfig;
+ getXAxis(layerConfig: LayerConfig, layerId: string) {
+ const { xAxisColumn } = layerConfig.reportConfig;
if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) {
- return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]);
+ return this.getBreakdownColumn({
+ layerId,
+ indexPattern: layerConfig.indexPattern,
+ sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0],
+ });
}
- return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label);
+ return this.getColumnBasedOnType({
+ layerConfig,
+ label: xAxisColumn.label,
+ sourceField: xAxisColumn.sourceField!,
+ });
}
- getColumnBasedOnType(
- sourceField: string,
- operationType?: OperationType,
- label?: string,
- colIndex?: number
- ) {
+ getColumnBasedOnType({
+ sourceField,
+ label,
+ layerConfig,
+ operationType,
+ colIndex,
+ }: {
+ sourceField: string;
+ operationType?: OperationType;
+ label?: string;
+ layerConfig: LayerConfig;
+ colIndex?: number;
+ }) {
const {
fieldMeta,
columnType,
fieldName,
- columnFilters,
- timeScale,
columnLabel,
- } = this.getFieldMeta(sourceField);
+ timeScale,
+ columnFilters,
+ } = this.getFieldMeta(sourceField, layerConfig);
const { type: fieldType } = fieldMeta ?? {};
if (columnType === TERMS_COLUMN) {
@@ -325,47 +351,76 @@ export class LensAttributes {
return this.getDateHistogramColumn(fieldName);
}
if (fieldType === 'number') {
- return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label);
+ return this.getNumberColumn({
+ sourceField: fieldName,
+ columnType,
+ operationType,
+ label: columnLabel || label,
+ reportViewConfig: layerConfig.reportConfig,
+ });
}
if (operationType === 'unique_count') {
- return this.getCardinalityColumn(fieldName, columnLabel || label);
+ return this.getCardinalityColumn({
+ sourceField: fieldName,
+ label: columnLabel || label,
+ reportViewConfig: layerConfig.reportConfig,
+ });
}
// FIXME review my approach again
return this.getDateHistogramColumn(fieldName);
}
- getCustomFieldName(sourceField: string) {
- return parseCustomFieldName(sourceField, this.reportViewConfig, this.reportDefinitions);
+ getCustomFieldName({
+ sourceField,
+ layerConfig,
+ }: {
+ sourceField: string;
+ layerConfig: LayerConfig;
+ }) {
+ return parseCustomFieldName(
+ sourceField,
+ layerConfig.reportConfig,
+ layerConfig.reportDefinitions
+ );
}
- getFieldMeta(sourceField: string) {
+ getFieldMeta(sourceField: string, layerConfig: LayerConfig) {
const {
fieldName,
columnType,
+ columnLabel,
columnFilters,
timeScale,
- columnLabel,
- } = this.getCustomFieldName(sourceField);
+ } = this.getCustomFieldName({
+ sourceField,
+ layerConfig,
+ });
- const fieldMeta = this.indexPattern.getFieldByName(fieldName);
+ const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName);
- return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel };
+ return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale };
}
- getMainYAxis() {
- const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumns[0];
+ getMainYAxis(layerConfig: LayerConfig) {
+ const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0];
if (sourceField === 'Records' || !sourceField) {
return this.getRecordsColumn(label);
}
- return this.getColumnBasedOnType(sourceField!, operationType, label, 0);
+ return this.getColumnBasedOnType({
+ sourceField,
+ operationType,
+ label,
+ layerConfig,
+ colIndex: 0,
+ });
}
- getChildYAxises() {
+ getChildYAxises(layerConfig: LayerConfig) {
const lensColumns: Record = {};
- const yAxisColumns = this.reportViewConfig.yAxisColumns;
+ const yAxisColumns = layerConfig.reportConfig.yAxisColumns;
// 1 means there is only main y axis
if (yAxisColumns.length === 1) {
return lensColumns;
@@ -373,12 +428,13 @@ export class LensAttributes {
for (let i = 1; i < yAxisColumns.length; i++) {
const { sourceField, operationType, label } = yAxisColumns[i];
- lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType(
- sourceField!,
+ lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType({
+ sourceField: sourceField!,
operationType,
label,
- i
- );
+ layerConfig,
+ colIndex: i,
+ });
}
return lensColumns;
}
@@ -396,20 +452,139 @@ export class LensAttributes {
scale: 'ratio',
sourceField: 'Records',
filter: columnFilter,
- timeScale,
+ ...(timeScale ? { timeScale } : {}),
} as CountIndexPatternColumn;
}
- getLayer() {
- return {
- columnOrder: ['x-axis-column', 'y-axis-column', ...Object.keys(this.getChildYAxises())],
- columns: {
- 'x-axis-column': this.getXAxis(),
- 'y-axis-column': this.getMainYAxis(),
- ...this.getChildYAxises(),
- },
- incompleteColumns: {},
- };
+ getLayerFilters(layerConfig: LayerConfig, totalLayers: number) {
+ const {
+ filters,
+ time: { from, to },
+ reportConfig: { filters: layerFilters, reportType },
+ } = layerConfig;
+ let baseFilters = '';
+ if (reportType !== 'kpi-over-time' && totalLayers > 1) {
+ // for kpi over time, we don't need to add time range filters
+ // since those are essentially plotted along the x-axis
+ baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`;
+ }
+
+ layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => {
+ const qFilter = filter as PersistableFilter;
+ if (qFilter.query?.match_phrase) {
+ const fieldName = Object.keys(qFilter.query.match_phrase)[0];
+ const kql = `${fieldName}: ${qFilter.query.match_phrase[fieldName]}`;
+ if (baseFilters.length > 0) {
+ baseFilters += ` and ${kql}`;
+ } else {
+ baseFilters += kql;
+ }
+ }
+ if (qFilter.query?.bool?.should) {
+ const values: string[] = [];
+ let fieldName = '';
+ qFilter.query?.bool.should.forEach((ft: PersistableFilter['query']['match_phrase']) => {
+ if (ft.match_phrase) {
+ fieldName = Object.keys(ft.match_phrase)[0];
+ values.push(ft.match_phrase[fieldName]);
+ }
+ });
+
+ const kueryString = `${fieldName}: (${values.join(' or ')})`;
+
+ if (baseFilters.length > 0) {
+ baseFilters += ` and ${kueryString}`;
+ } else {
+ baseFilters += kueryString;
+ }
+ }
+ const existFilter = filter as ExistsFilter;
+
+ if (existFilter.exists) {
+ const fieldName = existFilter.exists.field;
+ const kql = `${fieldName} : *`;
+ if (baseFilters.length > 0) {
+ baseFilters += ` and ${kql}`;
+ } else {
+ baseFilters += kql;
+ }
+ }
+ });
+
+ const rFilters = urlFiltersToKueryString(filters ?? []);
+ if (!baseFilters) {
+ return rFilters;
+ }
+ if (!rFilters) {
+ return baseFilters;
+ }
+ return `${rFilters} and ${baseFilters}`;
+ }
+
+ getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) {
+ if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') {
+ return null;
+ }
+
+ const {
+ time: { from: mainFrom },
+ } = mainLayerConfig;
+
+ const {
+ time: { from },
+ } = layerConfig;
+
+ const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days');
+ if (inDays > 1) {
+ return inDays + 'd';
+ }
+ const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours');
+ return inHours + 'h';
+ }
+
+ getLayers() {
+ const layers: Record = {};
+ const layerConfigs = this.layerConfigs;
+
+ layerConfigs.forEach((layerConfig, index) => {
+ const { breakdown } = layerConfig;
+
+ const layerId = `layer${index}`;
+ const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length);
+ const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index);
+ const mainYAxis = this.getMainYAxis(layerConfig);
+ layers[layerId] = {
+ columnOrder: [
+ `x-axis-column-${layerId}`,
+ ...(breakdown ? [`breakdown-column-${layerId}`] : []),
+ `y-axis-column-${layerId}`,
+ ...Object.keys(this.getChildYAxises(layerConfig)),
+ ],
+ columns: {
+ [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId),
+ [`y-axis-column-${layerId}`]: {
+ ...mainYAxis,
+ label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label,
+ filter: { query: columnFilter, language: 'kuery' },
+ ...(timeShift ? { timeShift } : {}),
+ },
+ ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN
+ ? // do nothing since this will be used a x axis source
+ {
+ [`breakdown-column-${layerId}`]: this.getBreakdownColumn({
+ layerId,
+ sourceField: breakdown,
+ indexPattern: layerConfig.indexPattern,
+ }),
+ }
+ : {}),
+ ...this.getChildYAxises(layerConfig),
+ },
+ incompleteColumns: {},
+ };
+ });
+
+ return layers;
}
getXyState(): XYState {
@@ -422,71 +597,48 @@ export class LensAttributes {
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
preferredSeriesType: 'line',
- layers: [
- {
- accessors: ['y-axis-column', ...Object.keys(this.getChildYAxises())],
- layerId: 'layer1',
- seriesType: this.seriesType ?? 'line',
- palette: this.reportViewConfig.palette,
- yConfig: this.reportViewConfig.yConfig || [
- { forAccessor: 'y-axis-column', color: 'green' },
- ],
- xAccessor: 'x-axis-column',
- },
- ],
- ...(this.reportViewConfig.yTitle ? { yTitle: this.reportViewConfig.yTitle } : {}),
+ layers: this.layerConfigs.map((layerConfig, index) => ({
+ accessors: [
+ `y-axis-column-layer${index}`,
+ ...Object.keys(this.getChildYAxises(layerConfig)),
+ ],
+ layerId: `layer${index}`,
+ seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType,
+ palette: layerConfig.reportConfig.palette,
+ yConfig: layerConfig.reportConfig.yConfig || [
+ { forAccessor: `y-axis-column-layer${index}` },
+ ],
+ xAccessor: `x-axis-column-layer${index}`,
+ ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}),
+ })),
+ ...(this.layerConfigs[0].reportConfig.yTitle
+ ? { yTitle: this.layerConfigs[0].reportConfig.yTitle }
+ : {}),
};
}
- parseFilters() {
- const defaultFilters = this.reportViewConfig.filters ?? [];
- const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : [];
-
- this.filters.forEach(({ field, values = [], notValues = [] }) => {
- const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!;
-
- if (values?.length > 0) {
- if (values?.length > 1) {
- const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern);
- parsedFilters.push(multiFilter);
- } else {
- const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern);
- parsedFilters.push(filter);
- }
- }
-
- if (notValues?.length > 0) {
- if (notValues?.length > 1) {
- const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern);
- multiFilter.meta.negate = true;
- parsedFilters.push(multiFilter);
- } else {
- const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern);
- filter.meta.negate = true;
- parsedFilters.push(filter);
- }
- }
- });
-
- return parsedFilters;
- }
+ parseFilters() {}
getJSON(): TypedLensByValueInput['attributes'] {
+ const uniqueIndexPatternsIds = Array.from(
+ new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)])
+ );
+
return {
title: 'Prefilled from exploratory view app',
description: '',
visualizationType: 'lnsXY',
references: [
- {
- id: this.indexPattern.id!,
+ ...uniqueIndexPatternsIds.map((patternId) => ({
+ id: patternId!,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
- },
- {
- id: this.indexPattern.id!,
- name: getLayerReferenceName('layer1'),
+ })),
+ ...this.layerConfigs.map(({ indexPattern }, index) => ({
+ id: indexPattern.id!,
+ name: getLayerReferenceName(`layer${index}`),
type: 'index-pattern',
- },
+ })),
],
state: {
datasourceStates: {
@@ -496,7 +648,7 @@ export class LensAttributes {
},
visualization: this.visualization,
query: { query: '', language: 'kuery' },
- filters: this.parseFilters(),
+ filters: [],
},
};
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
index 6f9806660e489..e1cb5a0370fb2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts
@@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields';
export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
return {
- reportType: 'mobile-device-distribution',
+ reportType: 'device-data-distribution',
defaultSeriesType: 'bar',
seriesTypes: ['bar', 'bar_horizontal'],
xAxisColumn: {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
index 854f844db047d..b958c0dd71528 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts
@@ -10,10 +10,10 @@ import { FieldLabels, RECORDS_FIELD } from '../constants';
import { buildExistsFilter } from '../utils';
import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels';
-export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
+export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries {
return {
reportType: 'data-distribution',
- defaultSeriesType: 'line',
+ defaultSeriesType: series?.seriesType || 'line',
seriesTypes: [],
xAxisColumn: {
sourceField: 'performance.metric',
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
index 9b299e7d70bcc..edf2a42415820 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts
@@ -10,16 +10,16 @@ export const sampleAttribute = {
visualizationType: 'lnsXY',
references: [
{ id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
- { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' },
+ { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
],
state: {
datasourceStates: {
indexpattern: {
layers: {
- layer1: {
- columnOrder: ['x-axis-column', 'y-axis-column'],
+ layer0: {
+ columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
columns: {
- 'x-axis-column': {
+ 'x-axis-column-layer0': {
sourceField: 'transaction.duration.us',
label: 'Page load time',
dataType: 'number',
@@ -32,13 +32,18 @@ export const sampleAttribute = {
maxBars: 'auto',
},
},
- 'y-axis-column': {
+ 'y-axis-column-layer0': {
dataType: 'number',
isBucketed: false,
label: 'Pages loaded',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
+ filter: {
+ language: 'kuery',
+ query:
+ 'transaction.type: page-load and processor.event: transaction and transaction.type : *',
+ },
},
},
incompleteColumns: {},
@@ -57,18 +62,15 @@ export const sampleAttribute = {
preferredSeriesType: 'line',
layers: [
{
- accessors: ['y-axis-column'],
- layerId: 'layer1',
+ accessors: ['y-axis-column-layer0'],
+ layerId: 'layer0',
seriesType: 'line',
- yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }],
- xAccessor: 'x-axis-column',
+ yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
+ xAccessor: 'x-axis-column-layer0',
},
],
},
query: { query: '', language: 'kuery' },
- filters: [
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
- { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
- ],
+ filters: [],
},
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
index fc60800bc4403..9b1e7ec141ca2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts
@@ -5,11 +5,12 @@
* 2.0.
*/
import rison, { RisonValue } from 'rison-node';
+import type { SeriesUrl, UrlFilter } from '../types';
import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage';
-import type { SeriesUrl } from '../types';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
-import { esFilters } from '../../../../../../../../src/plugins/data/public';
+import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public';
import { URL_KEYS } from './constants/url_constants';
+import { PersistableFilter } from '../../../../../../lens/common';
export function convertToShortUrl(series: SeriesUrl) {
const {
@@ -51,7 +52,7 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
}
export function buildPhraseFilter(field: string, value: string, indexPattern: IIndexPattern) {
- const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
+ const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta) {
return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)];
}
@@ -59,7 +60,7 @@ export function buildPhraseFilter(field: string, value: string, indexPattern: II
}
export function buildPhrasesFilter(field: string, value: string[], indexPattern: IIndexPattern) {
- const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
+ const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta) {
return [esFilters.buildPhrasesFilter(fieldMeta, value, indexPattern)];
}
@@ -67,9 +68,38 @@ export function buildPhrasesFilter(field: string, value: string[], indexPattern:
}
export function buildExistsFilter(field: string, indexPattern: IIndexPattern) {
- const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
+ const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta) {
return [esFilters.buildExistsFilter(fieldMeta, indexPattern)];
}
return [];
}
+
+type FiltersType = PersistableFilter[] | ExistsFilter[];
+
+export function urlFilterToPersistedFilter({
+ urlFilters,
+ initFilters,
+ indexPattern,
+}: {
+ urlFilters: UrlFilter[];
+ initFilters: FiltersType;
+ indexPattern: IIndexPattern;
+}) {
+ const parsedFilters: FiltersType = initFilters ? [...initFilters] : [];
+
+ urlFilters.forEach(({ field, values = [], notValues = [] }) => {
+ if (values?.length > 0) {
+ const filter = buildPhrasesFilter(field, values, indexPattern);
+ parsedFilters.push(...filter);
+ }
+
+ if (notValues?.length > 0) {
+ const filter = buildPhrasesFilter(field, notValues, indexPattern)[0];
+ filter.meta.negate = true;
+ parsedFilters.push(filter);
+ }
+ });
+
+ return parsedFilters;
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
index 779049601bd6d..989ebf17c2062 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx
@@ -51,8 +51,9 @@ describe('ExploratoryView', () => {
const initSeries = {
data: {
'ux-series': {
+ isNew: true,
dataType: 'ux' as const,
- reportType: 'dist' as const,
+ reportType: 'data-distribution' as const,
breakdown: 'user_agent .name',
reportDefinitions: { 'service.name': ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index 329ed20ffed3d..ad85ecab968b2 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -5,9 +5,10 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useEffect, useRef, useState, useCallback } from 'react';
import { EuiPanel, EuiTitle } from '@elastic/eui';
import styled from 'styled-components';
+import { isEmpty } from 'lodash';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { ExploratoryViewHeader } from './header/header';
@@ -17,10 +18,37 @@ import { EmptyView } from './components/empty_view';
import { TypedLensByValueInput } from '../../../../../lens/public';
import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
import { SeriesBuilder } from './series_builder/series_builder';
+import { SeriesUrl } from './types';
+
+export const combineTimeRanges = (
+ allSeries: Record,
+ firstSeries?: SeriesUrl
+) => {
+ let to: string = '';
+ let from: string = '';
+ if (firstSeries?.reportType === 'kpi-over-time') {
+ return firstSeries.time;
+ }
+ Object.values(allSeries ?? {}).forEach((series) => {
+ if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) {
+ const seriesTo = new Date(series.time.to);
+ const seriesFrom = new Date(series.time.from);
+ if (!to || seriesTo > new Date(to)) {
+ to = series.time.to;
+ }
+ if (!from || seriesFrom < new Date(from)) {
+ from = series.time.from;
+ }
+ }
+ });
+ return { to, from };
+};
export function ExploratoryView({
saveAttributes,
+ multiSeries,
}: {
+ multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
const {
@@ -33,6 +61,8 @@ export function ExploratoryView({
const [height, setHeight] = useState('100vh');
const [seriesId, setSeriesId] = useState('');
+ const [lastUpdated, setLastUpdated] = useState();
+
const [lensAttributes, setLensAttributes] = useState(
null
);
@@ -47,9 +77,7 @@ export function ExploratoryView({
setSeriesId(firstSeriesId);
}, [allSeries, firstSeriesId]);
- const lensAttributesT = useLensAttributes({
- seriesId,
- });
+ const lensAttributesT = useLensAttributes();
const setHeightOffset = () => {
if (seriesBuilderRef?.current && wrapperRef.current) {
@@ -60,10 +88,12 @@ export function ExploratoryView({
};
useEffect(() => {
- if (series?.dataType) {
- loadIndexPattern({ dataType: series?.dataType });
- }
- }, [series?.dataType, loadIndexPattern]);
+ Object.values(allSeries).forEach((seriesT) => {
+ loadIndexPattern({
+ dataType: seriesT.dataType,
+ });
+ });
+ }, [allSeries, loadIndexPattern]);
useEffect(() => {
setLensAttributes(lensAttributesT);
@@ -72,47 +102,62 @@ export function ExploratoryView({
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]);
+ }, [JSON.stringify(lensAttributesT ?? {})]);
useEffect(() => {
setHeightOffset();
});
+ const timeRange = combineTimeRanges(allSeries, series);
+
+ const onLensLoad = useCallback(() => {
+ setLastUpdated(Date.now());
+ }, []);
+
+ const onBrushEnd = useCallback(
+ ({ range }: { range: number[] }) => {
+ if (series?.reportType !== 'data-distribution') {
+ setSeries(seriesId, {
+ ...series,
+ time: {
+ from: new Date(range[0]).toISOString(),
+ to: new Date(range[1]).toISOString(),
+ },
+ });
+ } else {
+ notifications?.toasts.add(
+ i18n.translate('xpack.observability.exploratoryView.noBrusing', {
+ defaultMessage: 'Zoom by brush selection is only available on time series charts.',
+ })
+ );
+ }
+ },
+ [notifications?.toasts, series, seriesId, setSeries]
+ );
+
return (
{lens ? (
<>
- {lensAttributes && seriesId && series?.reportType && series?.time ? (
+ {lensAttributes && timeRange.to && timeRange.from ? (
{
- if (series?.reportType !== 'dist') {
- setSeries(seriesId, {
- ...series,
- time: {
- from: new Date(range[0]).toISOString(),
- to: new Date(range[1]).toISOString(),
- },
- });
- } else {
- notifications?.toasts.add(
- i18n.translate('xpack.observability.exploratoryView.noBrusing', {
- defaultMessage:
- 'Zoom by brush selection is only available on time series charts.',
- })
- );
- }
- }}
+ onLoad={onLensLoad}
+ onBrushEnd={onBrushEnd}
/>
) : (
)}
-
+
>
) : (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
index 1dedc4142f174..8cd8977fcf741 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx
@@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () {
data: {
'uptime-pings-histogram': {
dataType: 'synthetics' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index 3e02207e26272..dbe9cd163451d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ
import { DataViewLabels } from '../configurations/constants';
import { ObservabilityAppServices } from '../../../../application/types';
import { useSeriesStorage } from '../hooks/use_series_storage';
+import { combineTimeRanges } from '../exploratory_view';
interface Props {
seriesId: string;
@@ -24,7 +25,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
const { lens } = kServices;
- const { getSeries } = useSeriesStorage();
+ const { getSeries, allSeries } = useSeriesStorage();
const series = getSeries(seriesId);
@@ -32,6 +33,8 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
const LensSaveModalComponent = lens.SaveModalComponent;
+ const timeRange = combineTimeRanges(allSeries, series);
+
return (
<>
@@ -63,7 +66,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
lens.navigateToPrefilledEditor(
{
id: '',
- timeRange: series.time,
+ timeRange,
attributes: lensAttributes,
},
true
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
index 4259bb778e511..7a5f12a72b1f0 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx
@@ -15,7 +15,6 @@ import { getDataHandler } from '../../../../data_handler';
export interface IIndexPatternContext {
loading: boolean;
- selectedApp: AppDataType;
indexPatterns: IndexPatternState;
hasAppData: HasAppDataState;
loadIndexPattern: (params: { dataType: AppDataType }) => void;
@@ -29,10 +28,10 @@ interface ProviderProps {
type HasAppDataState = Record;
type IndexPatternState = Record;
+type LoadingState = Record;
export function IndexPatternContextProvider({ children }: ProviderProps) {
- const [loading, setLoading] = useState(false);
- const [selectedApp, setSelectedApp] = useState();
+ const [loading, setLoading] = useState({} as LoadingState);
const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState);
const [hasAppData, setHasAppData] = useState({
infra_metrics: null,
@@ -49,10 +48,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback(
async ({ dataType }) => {
- setSelectedApp(dataType);
+ if (hasAppData[dataType] === null && !loading[dataType]) {
+ setLoading((prevState) => ({ ...prevState, [dataType]: true }));
- if (hasAppData[dataType] === null) {
- setLoading(true);
try {
let hasDataT = false;
let indices: string | undefined = '';
@@ -78,23 +76,22 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern }));
}
- setLoading(false);
+ setLoading((prevState) => ({ ...prevState, [dataType]: false }));
} catch (e) {
- setLoading(false);
+ setLoading((prevState) => ({ ...prevState, [dataType]: false }));
}
}
},
- [data, hasAppData]
+ [data, hasAppData, loading]
);
return (
loadingT),
}}
>
{children}
@@ -102,19 +99,23 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
);
}
-export const useAppIndexPatternContext = () => {
- const { selectedApp, loading, hasAppData, loadIndexPattern, indexPatterns } = useContext(
+export const useAppIndexPatternContext = (dataType?: AppDataType) => {
+ const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext(
(IndexPatternContext as unknown) as Context
);
+ if (dataType && !indexPatterns?.[dataType] && !loading) {
+ loadIndexPattern({ dataType });
+ }
+
return useMemo(() => {
return {
hasAppData,
- selectedApp,
loading,
- indexPattern: indexPatterns?.[selectedApp],
- hasData: hasAppData?.[selectedApp],
+ indexPatterns,
+ indexPattern: dataType ? indexPatterns?.[dataType] : undefined,
+ hasData: dataType ? hasAppData?.[dataType] : undefined,
loadIndexPattern,
};
- }, [hasAppData, indexPatterns, loadIndexPattern, loading, selectedApp]);
+ }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]);
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
index 1c85bc5089b2a..11487afe28e96 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts
@@ -8,17 +8,13 @@
import { useMemo } from 'react';
import { isEmpty } from 'lodash';
import { TypedLensByValueInput } from '../../../../../../lens/public';
-import { LensAttributes } from '../configurations/lens_attributes';
+import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
import { useSeriesStorage } from './use_series_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
import { DataSeries, SeriesUrl, UrlFilter } from '../types';
import { useAppIndexPatternContext } from './use_app_index_pattern';
-interface Props {
- seriesId: string;
-}
-
export const getFiltersFromDefs = (
reportDefinitions: SeriesUrl['reportDefinitions'],
dataViewConfig: DataSeries
@@ -37,54 +33,51 @@ export const getFiltersFromDefs = (
});
};
-export const useLensAttributes = ({
- seriesId,
-}: Props): TypedLensByValueInput['attributes'] | null => {
- const { getSeries } = useSeriesStorage();
- const series = getSeries(seriesId);
- const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } =
- series ?? {};
+export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => {
+ const { allSeriesIds, allSeries } = useSeriesStorage();
- const { indexPattern } = useAppIndexPatternContext();
+ const { indexPatterns } = useAppIndexPatternContext();
return useMemo(() => {
- if (!indexPattern || !reportType || isEmpty(reportDefinitions)) {
+ if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) {
return null;
}
- const dataViewConfig = getDefaultConfigs({
- reportType,
- dataType,
- indexPattern,
- });
+ const layerConfigs: LayerConfig[] = [];
+
+ allSeriesIds.forEach((seriesIdT) => {
+ const seriesT = allSeries[seriesIdT];
+ const indexPattern = indexPatterns?.[seriesT?.dataType];
+ if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) {
+ const reportViewConfig = getDefaultConfigs({
+ reportType: seriesT.reportType,
+ dataType: seriesT.dataType,
+ indexPattern,
+ });
- const filters: UrlFilter[] = (series.filters ?? []).concat(
- getFiltersFromDefs(reportDefinitions, dataViewConfig)
- );
+ const filters: UrlFilter[] = (seriesT.filters ?? []).concat(
+ getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig)
+ );
- const lensAttributes = new LensAttributes(
- indexPattern,
- dataViewConfig,
- seriesType,
- filters,
- operationType,
- reportDefinitions,
- breakdown
- );
+ layerConfigs.push({
+ filters,
+ indexPattern,
+ reportConfig: reportViewConfig,
+ breakdown: seriesT.breakdown,
+ operationType: seriesT.operationType,
+ seriesType: seriesT.seriesType,
+ reportDefinitions: seriesT.reportDefinitions ?? {},
+ time: seriesT.time,
+ });
+ }
+ });
- if (breakdown) {
- lensAttributes.addBreakdown(breakdown);
+ if (layerConfigs.length < 1) {
+ return null;
}
+ const lensAttributes = new LensAttributes(layerConfigs);
+
return lensAttributes.getJSON();
- }, [
- indexPattern,
- reportType,
- reportDefinitions,
- dataType,
- series.filters,
- seriesType,
- operationType,
- breakdown,
- ]);
+ }, [indexPatterns, allSeriesIds, allSeries]);
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
index fac75f910a93f..e9ae43950d47d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx
@@ -12,7 +12,7 @@ import {
} from '../../../../../../../../src/plugins/kibana_utils/public';
import type {
AppDataType,
- ReportViewTypeId,
+ ReportViewType,
SeriesUrl,
UrlFilter,
URLReportDefinition,
@@ -36,6 +36,16 @@ interface ProviderProps {
storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
}
+function convertAllShortSeries(allShortSeries: AllShortSeries) {
+ const allSeriesIds = Object.keys(allShortSeries);
+ const allSeriesN: AllSeries = {};
+ allSeriesIds.forEach((seriesKey) => {
+ allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
+ });
+
+ return allSeriesN;
+}
+
export function UrlStorageContextProvider({
children,
storage,
@@ -45,15 +55,14 @@ export function UrlStorageContextProvider({
const [allShortSeries, setAllShortSeries] = useState(
() => storage.get(allSeriesKey) ?? {}
);
- const [allSeries, setAllSeries] = useState({});
+ const [allSeries, setAllSeries] = useState(() =>
+ convertAllShortSeries(storage.get(allSeriesKey) ?? {})
+ );
const [firstSeriesId, setFirstSeriesId] = useState('');
useEffect(() => {
const allSeriesIds = Object.keys(allShortSeries);
- const allSeriesN: AllSeries = {};
- allSeriesIds.forEach((seriesKey) => {
- allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
- });
+ const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {});
setAllSeries(allSeriesN);
setFirstSeriesId(allSeriesIds?.[0]);
@@ -68,8 +77,10 @@ export function UrlStorageContextProvider({
};
const removeSeries = (seriesIdN: string) => {
- delete allShortSeries[seriesIdN];
- delete allSeries[seriesIdN];
+ setAllShortSeries((prevState) => {
+ delete prevState[seriesIdN];
+ return { ...prevState };
+ });
};
const allSeriesIds = Object.keys(allShortSeries);
@@ -115,7 +126,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
interface ShortUrlSeries {
[URL_KEYS.OPERATION_TYPE]?: OperationType;
- [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId;
+ [URL_KEYS.REPORT_TYPE]?: ReportViewType;
[URL_KEYS.DATA_TYPE]?: AppDataType;
[URL_KEYS.SERIES_TYPE]?: SeriesType;
[URL_KEYS.BREAK_DOWN]?: string;
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
index 3de29b02853e8..e55752ceb62ba 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx
@@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public';
export function ExploratoryViewPage({
saveAttributes,
+ multiSeries = false,
useSessionStorage = false,
}: {
useSessionStorage?: boolean;
+ multiSeries?: boolean;
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' });
@@ -59,7 +61,7 @@ export function ExploratoryViewPage({
-
+
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
index 8e54ab7629d26..972e3beb4b722 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx
@@ -35,8 +35,11 @@ import { getStubIndexPattern } from '../../../../../../../src/plugins/data/publi
import indexPatternData from './configurations/test_data/test_index_pattern.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services';
-import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
-import { UrlFilter } from './types';
+import {
+ IndexPattern,
+ IndexPatternsContract,
+} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
+import { AppDataType, UrlFilter } from './types';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { ListItem } from '../../../hooks/use_values_list';
@@ -232,11 +235,11 @@ export const mockAppIndexPattern = () => {
const loadIndexPattern = jest.fn();
const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({
indexPattern: mockIndexPattern,
- selectedApp: 'ux',
hasData: true,
loading: false,
hasAppData: { ux: true } as any,
loadIndexPattern,
+ indexPatterns: ({ ux: mockIndexPattern } as unknown) as Record,
});
return { spy, loadIndexPattern };
};
@@ -260,7 +263,7 @@ function mockSeriesStorageContext({
}) {
const mockDataSeries = data || {
'performance-distribution': {
- reportType: 'dist',
+ reportType: 'data-distribution',
dataType: 'ux',
breakdown: breakdown || 'user_agent.name',
time: { from: 'now-15m', to: 'now' },
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
index 9ae8b68bf3e8c..50c2f91e6067d 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx
@@ -27,18 +27,14 @@ export function SeriesChartTypesSelect({
seriesTypes?: SeriesType[];
defaultChartType: SeriesType;
}) {
- const { getSeries, setSeries, allSeries } = useSeriesStorage();
+ const { getSeries, setSeries } = useSeriesStorage();
const series = getSeries(seriesId);
const seriesType = series?.seriesType ?? defaultChartType;
const onChange = (value: SeriesType) => {
- Object.keys(allSeries).forEach((seriesKey) => {
- const seriesN = allSeries[seriesKey];
-
- setSeries(seriesKey, { ...seriesN, seriesType: value });
- });
+ setSeries(seriesId, { ...series, seriesType: value });
};
return (
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
index e3c1666c533ef..b10702ebded57 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx
@@ -29,7 +29,14 @@ describe('DataTypesCol', function () {
fireEvent.click(screen.getByText(/user experience \(rum\)/i));
expect(setSeries).toHaveBeenCalledTimes(1);
- expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux' });
+ expect(setSeries).toHaveBeenCalledWith(seriesId, {
+ dataType: 'ux',
+ isNew: true,
+ time: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ });
});
it('should set series on change on already selected', function () {
@@ -37,7 +44,7 @@ describe('DataTypesCol', function () {
data: {
[seriesId]: {
dataType: 'synthetics' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
index 985afdf888868..f386f62d9ed73 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx
@@ -31,7 +31,11 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) {
if (!dataType) {
removeSeries(seriesId);
} else {
- setSeries(seriesId || `${dataType}-series`, { dataType } as any);
+ setSeries(seriesId || `${dataType}-series`, {
+ dataType,
+ isNew: true,
+ time: series.time,
+ } as any);
}
};
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
index 175fbea9445c1..6be78084ae195 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx
@@ -8,14 +8,23 @@
import React from 'react';
import styled from 'styled-components';
import { SeriesDatePicker } from '../../series_date_picker';
+import { DateRangePicker } from '../../series_date_picker/date_range_picker';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
interface Props {
seriesId: string;
}
export function DatePickerCol({ seriesId }: Props) {
+ const { firstSeriesId, getSeries } = useSeriesStorage();
+ const { reportType } = getSeries(firstSeriesId);
+
return (
-
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
index c262a94f968be..516f04e3812ba 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx
@@ -22,7 +22,7 @@ describe('OperationTypeSelect', function () {
data: {
'performance-distribution': {
dataType: 'ux' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
@@ -39,7 +39,7 @@ describe('OperationTypeSelect', function () {
data: {
'series-id': {
dataType: 'ux' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
operationType: 'median' as const,
time: { from: 'now-15m', to: 'now' },
},
@@ -53,7 +53,7 @@ describe('OperationTypeSelect', function () {
expect(setSeries).toHaveBeenCalledWith('series-id', {
operationType: 'median',
dataType: 'ux',
- reportType: 'kpi',
+ reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
});
@@ -61,7 +61,7 @@ describe('OperationTypeSelect', function () {
expect(setSeries).toHaveBeenCalledWith('series-id', {
operationType: '95th',
dataType: 'ux',
- reportType: 'kpi',
+ reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
index 805186e877d57..203382afc1624 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx
@@ -15,7 +15,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel
describe('Series Builder ReportBreakdowns', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
dataType: 'ux',
indexPattern: mockIndexPattern,
});
@@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
breakdown: USER_AGENT_OS,
dataType: 'ux',
- reportType: 'dist',
+ reportType: 'data-distribution',
time: { from: 'now-15m', to: 'now' },
});
});
@@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
breakdown: undefined,
dataType: 'ux',
- reportType: 'dist',
+ reportType: 'data-distribution',
time: { from: 'now-15m', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
index e947961fb4300..2e5c674b9fad8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx
@@ -22,7 +22,7 @@ describe('Series Builder ReportDefinitionCol', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
indexPattern: mockIndexPattern,
dataType: 'ux',
});
@@ -31,7 +31,7 @@ describe('Series Builder ReportDefinitionCol', function () {
data: {
[seriesId]: {
dataType: 'ux' as const,
- reportType: 'dist' as const,
+ reportType: 'data-distribution' as const,
time: { from: 'now-30d', to: 'now' },
reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
},
@@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
dataType: 'ux',
reportDefinitions: {},
- reportType: 'dist',
+ reportType: 'data-distribution',
time: { from: 'now-30d', to: 'now' },
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
index 338f5d52c26fa..47962af0d4bc4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import styled from 'styled-components';
-import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { CustomReportField } from '../custom_report_field';
import { DataSeries, URLReportDefinition } from '../../types';
@@ -36,8 +35,6 @@ export function ReportDefinitionCol({
dataViewSeries: DataSeries;
seriesId: string;
}) {
- const { indexPattern } = useAppIndexPatternContext();
-
const { getSeries, setSeries } = useSeriesStorage();
const series = getSeries(seriesId);
@@ -69,21 +66,20 @@ export function ReportDefinitionCol({
- {indexPattern &&
- reportDefinitions.map(({ field, custom, options }) => (
-
- {!custom ? (
-
- ) : (
-
- )}
-
- ))}
+ {reportDefinitions.map(({ field, custom, options }) => (
+
+ {!custom ? (
+
+ ) : (
+
+ )}
+
+ ))}
{(hasOperationType || columnType === 'operation') && (
{
- if (!custom && selectedReportDefinitions?.[fieldT] && fieldT !== field) {
+ if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) {
const values = selectedReportDefinitions?.[fieldT];
const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0];
filtersN.push(valueFilter.query);
@@ -64,16 +64,18 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }:
return (
- onChange(field, val)}
- filters={queryFilters}
- time={series.time}
- fullWidth={true}
- />
+ {indexPattern && (
+ onChange(field, val)}
+ filters={queryFilters}
+ time={series.time}
+ fullWidth={true}
+ />
+ )}
);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
index 7ca947fed0bc9..f35639388aac5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx
@@ -15,7 +15,7 @@ describe('Series Builder ReportFilters', function () {
const seriesId = 'test-series-id';
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
indexPattern: mockIndexPattern,
dataType: 'ux',
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
index f36d64ca5bbbd..f7cfe06c0d928 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx
@@ -11,10 +11,9 @@ import { mockAppIndexPattern, render } from '../../rtl_helpers';
import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col';
import { ReportTypes } from '../series_builder';
import { DEFAULT_TIME } from '../../configurations/constants';
-import { NEW_SERIES_KEY } from '../../hooks/use_series_storage';
describe('ReportTypesCol', function () {
- const seriesId = 'test-series-id';
+ const seriesId = 'performance-distribution';
mockAppIndexPattern();
@@ -40,7 +39,7 @@ describe('ReportTypesCol', function () {
breakdown: 'user_agent.name',
dataType: 'ux',
reportDefinitions: {},
- reportType: 'kpi',
+ reportType: 'kpi-over-time',
time: { from: 'now-15m', to: 'now' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
@@ -49,11 +48,12 @@ describe('ReportTypesCol', function () {
it('should set selected as filled', function () {
const initSeries = {
data: {
- [NEW_SERIES_KEY]: {
+ [seriesId]: {
dataType: 'synthetics' as const,
- reportType: 'kpi' as const,
+ reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
+ isNew: true,
},
},
};
@@ -74,6 +74,7 @@ describe('ReportTypesCol', function () {
expect(setSeries).toHaveBeenCalledWith(seriesId, {
dataType: 'synthetics',
time: DEFAULT_TIME,
+ isNew: true,
});
});
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
index 9fff8dae14a47..64c7b48c668b8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx
@@ -7,27 +7,33 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
+import { map } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import styled from 'styled-components';
-import { ReportViewTypeId, SeriesUrl } from '../../types';
+import { ReportViewType, SeriesUrl } from '../../types';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { DEFAULT_TIME } from '../../configurations/constants';
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
+import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder';
interface Props {
seriesId: string;
- reportTypes: Array<{ id: ReportViewTypeId; label: string }>;
+ reportTypes: ReportTypeItem[];
}
export function ReportTypesCol({ seriesId, reportTypes }: Props) {
- const { setSeries, getSeries } = useSeriesStorage();
+ const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage();
const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId);
- const { loading, hasData, selectedApp } = useAppIndexPatternContext();
+ const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType);
- if (!loading && !hasData && selectedApp) {
+ if (!restSeries.dataType) {
+ return {SELECT_DATA_TYPE} ;
+ }
+
+ if (!loading && !hasData) {
return (
firstSeriesId !== seriesId && reportType !== firstSeries.reportType
+ ),
+ 'reportType'
+ );
+
return reportTypes?.length > 0 ? (
- {reportTypes.map(({ id: reportType, label }) => (
+ {reportTypes.map(({ reportType, label }) => (
{
if (reportType === selectedReportType) {
setSeries(seriesId, {
dataType: restSeries.dataType,
time: DEFAULT_TIME,
+ isNew: true,
} as SeriesUrl);
} else {
setSeries(seriesId, {
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
new file mode 100644
index 0000000000000..874171de123d2
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { EuiIcon, EuiText } from '@elastic/eui';
+import moment from 'moment';
+
+interface Props {
+ lastUpdated?: number;
+}
+export function LastUpdated({ lastUpdated }: Props) {
+ const [refresh, setRefresh] = useState(() => Date.now());
+
+ useEffect(() => {
+ const interVal = setInterval(() => {
+ setRefresh(Date.now());
+ }, 1000);
+
+ return () => {
+ clearInterval(interVal);
+ };
+ }, []);
+
+ if (!lastUpdated) {
+ return null;
+ }
+
+ return (
+
+ Last Updated: {moment(lastUpdated).from(refresh)}
+
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
index 9aef16931d7ec..e596eb6be354a 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx
@@ -5,11 +5,19 @@
* 2.0.
*/
-import React, { RefObject } from 'react';
-
+import React, { RefObject, useEffect, useState } from 'react';
+import { isEmpty } from 'lodash';
import { i18n } from '@kbn/i18n';
-import { EuiBasicTable } from '@elastic/eui';
-import { AppDataType, ReportViewTypeId, ReportViewTypes, SeriesUrl } from '../types';
+import {
+ EuiBasicTable,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiSwitch,
+} from '@elastic/eui';
+import { rgba } from 'polished';
+import { AppDataType, DataSeries, ReportViewType, SeriesUrl } from '../types';
import { DataTypesCol } from './columns/data_types_col';
import { ReportTypesCol } from './columns/report_types_col';
import { ReportDefinitionCol } from './columns/report_definition_col';
@@ -18,6 +26,10 @@ import { ReportBreakdowns } from './columns/report_breakdowns';
import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
import { getDefaultConfigs } from '../configurations/default_configs';
+import { SeriesEditor } from '../series_editor/series_editor';
+import { SeriesActions } from '../series_editor/columns/series_actions';
+import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
+import { LastUpdated } from './last_updated';
import {
CORE_WEB_VITALS_LABEL,
DEVICE_DISTRIBUTION_LABEL,
@@ -25,72 +37,94 @@ import {
PERF_DIST_LABEL,
} from '../configurations/constants/labels';
-export const ReportTypes: Record> = {
+export interface ReportTypeItem {
+ id: string;
+ reportType: ReportViewType;
+ label: string;
+}
+
+export const ReportTypes: Record = {
synthetics: [
- { id: 'kpi', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', label: PERF_DIST_LABEL },
+ { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
],
ux: [
- { id: 'kpi', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', label: PERF_DIST_LABEL },
- { id: 'cwv', label: CORE_WEB_VITALS_LABEL },
+ { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
+ { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
],
mobile: [
- { id: 'kpi', label: KPI_OVER_TIME_LABEL },
- { id: 'dist', label: PERF_DIST_LABEL },
- { id: 'mdd', label: DEVICE_DISTRIBUTION_LABEL },
+ { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
+ { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
+ { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
],
apm: [],
infra_logs: [],
infra_metrics: [],
};
+interface BuilderItem {
+ id: string;
+ series: SeriesUrl;
+ seriesConfig?: DataSeries;
+}
+
export function SeriesBuilder({
seriesBuilderRef,
- seriesId,
+ lastUpdated,
+ multiSeries,
}: {
- seriesId: string;
seriesBuilderRef: RefObject;
+ lastUpdated?: number;
+ multiSeries?: boolean;
}) {
- const { getSeries, setSeries, removeSeries } = useSeriesStorage();
-
- const series = getSeries(seriesId);
-
- const {
- dataType,
- seriesType,
- reportType,
- reportDefinitions = {},
- filters = [],
- operationType,
- breakdown,
- time,
- } = series;
-
- const { indexPattern, loading, hasData } = useAppIndexPatternContext();
-
- const getDataViewSeries = () => {
- return getDefaultConfigs({
- dataType,
- indexPattern,
- reportType: reportType!,
- });
- };
+ const [editorItems, setEditorItems] = useState([]);
+ const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage();
+
+ const { loading, indexPatterns } = useAppIndexPatternContext();
+
+ useEffect(() => {
+ const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => {
+ if (indexPatterns?.[dataType]) {
+ return getDefaultConfigs({
+ dataType,
+ indexPattern: indexPatterns[dataType],
+ reportType: reportType!,
+ });
+ }
+ };
+
+ const seriesToEdit: BuilderItem[] =
+ allSeriesIds
+ .filter((sId) => {
+ return allSeries?.[sId]?.isNew;
+ })
+ .map((sId) => {
+ const series = getSeries(sId);
+ const seriesConfig = getDataViewSeries(series.dataType, series.reportType);
+
+ return { id: sId, series, seriesConfig };
+ }) ?? [];
+ const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }];
+ setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries);
+ }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]);
const columns = [
{
name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', {
defaultMessage: 'Data Type',
}),
+ field: 'id',
width: '15%',
- render: (val: string) => ,
+ render: (seriesId: string) => ,
},
{
name: i18n.translate('xpack.observability.expView.seriesBuilder.report', {
defaultMessage: 'Report',
}),
width: '15%',
- render: (val: string) => (
+ field: 'id',
+ render: (seriesId: string, { series: { dataType } }: BuilderItem) => (
),
},
@@ -99,12 +133,16 @@ export function SeriesBuilder({
defaultMessage: 'Definition',
}),
width: '30%',
- render: (val: string) => {
- if (dataType && hasData) {
+ field: 'id',
+ render: (
+ seriesId: string,
+ { series: { dataType, reportType }, seriesConfig }: BuilderItem
+ ) => {
+ if (dataType && seriesConfig) {
return loading ? (
LOADING_VIEW
) : reportType ? (
-
+
) : (
SELECT_REPORT_TYPE
);
@@ -118,9 +156,10 @@ export function SeriesBuilder({
defaultMessage: 'Filters',
}),
width: '20%',
- render: (val: string) =>
- reportType && indexPattern ? (
-
+ field: 'id',
+ render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
+ reportType && seriesConfig ? (
+
) : null,
},
{
@@ -129,53 +168,126 @@ export function SeriesBuilder({
}),
width: '20%',
field: 'id',
- render: (val: string) =>
- reportType && indexPattern ? (
-
+ render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
+ reportType && seriesConfig ? (
+
) : null,
},
+ ...(multiSeries
+ ? [
+ {
+ name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', {
+ defaultMessage: 'Actions',
+ }),
+ align: 'center' as const,
+ width: '10%',
+ field: 'id',
+ render: (seriesId: string, item: BuilderItem) => (
+
+ ),
+ },
+ ]
+ : []),
];
- // TODO: Remove this if remain unused during multiple series view
- // @ts-expect-error
- const addSeries = () => {
- if (reportType) {
- const newSeriesId = `${
- reportDefinitions?.['service.name'] ||
- reportDefinitions?.['monitor.id'] ||
- ReportViewTypes[reportType]
- }`;
-
- const newSeriesN: SeriesUrl = {
- dataType,
- time,
- filters,
- breakdown,
- reportType,
- seriesType,
- operationType,
- reportDefinitions,
- };
-
- setSeries(newSeriesId, newSeriesN);
- removeSeries(NEW_SERIES_KEY);
- }
+ const applySeries = () => {
+ editorItems.forEach(({ series, id: seriesId }) => {
+ const { reportType, reportDefinitions, isNew, ...restSeries } = series;
+
+ if (reportType && !isEmpty(reportDefinitions)) {
+ const reportDefId = Object.values(reportDefinitions ?? {})[0];
+ const newSeriesId = `${reportDefId}-${reportType}`;
+
+ const newSeriesN: SeriesUrl = {
+ ...restSeries,
+ reportType,
+ reportDefinitions,
+ };
+
+ setSeries(newSeriesId, newSeriesN);
+ removeSeries(seriesId);
+ }
+ });
};
- const items = [{ id: seriesId }];
+ const addSeries = () => {
+ const prevSeries = allSeries?.[allSeriesIds?.[0]];
+ setSeries(
+ `${NEW_SERIES_KEY}-${editorItems.length + 1}`,
+ prevSeries
+ ? ({ isNew: true, time: prevSeries.time } as SeriesUrl)
+ : ({ isNew: true } as SeriesUrl)
+ );
+ };
return (
-
-
-
+
+ {multiSeries && (
+
+
+
+
+
+ {}}
+ compressed
+ />
+
+
+ applySeries()} isDisabled={true} size="s">
+ {i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
+ defaultMessage: 'Apply changes',
+ })}
+
+
+
+ addSeries()} size="s">
+ {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
+ defaultMessage: 'Add Series',
+ })}
+
+
+
+ )}
+
+ {multiSeries && }
+ {editorItems.length > 0 && (
+
+ )}
+
+
+
);
}
+const Wrapper = euiStyled.div`
+ max-height: 50vh;
+ overflow-y: scroll;
+ overflow-x: clip;
+ &::-webkit-scrollbar {
+ height: ${({ theme }) => theme.eui.euiScrollBar};
+ width: ${({ theme }) => theme.eui.euiScrollBar};
+ }
+ &::-webkit-scrollbar-thumb {
+ background-clip: content-box;
+ background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
+ border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
+ }
+ &::-webkit-scrollbar-corner,
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+`;
+
export const LOADING_VIEW = i18n.translate(
'xpack.observability.expView.seriesBuilder.loadingView',
{
@@ -189,3 +301,10 @@ export const SELECT_REPORT_TYPE = i18n.translate(
defaultMessage: 'No report type selected',
}
);
+
+export const SELECT_DATA_TYPE = i18n.translate(
+ 'xpack.observability.expView.seriesBuilder.selectDataType',
+ {
+ defaultMessage: 'No data type selected',
+ }
+);
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
new file mode 100644
index 0000000000000..c30863585b3b0
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
+import DateMath from '@elastic/datemath';
+import { Moment } from 'moment';
+import { useSeriesStorage } from '../hooks/use_series_storage';
+import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public';
+
+export const parseAbsoluteDate = (date: string, options = {}) => {
+ return DateMath.parse(date, options)!;
+};
+export function DateRangePicker({ seriesId }: { seriesId: string }) {
+ const { firstSeriesId, getSeries, setSeries } = useSeriesStorage();
+ const dateFormat = useUiSetting('dateFormat');
+
+ const {
+ time: { from, to },
+ reportType,
+ } = getSeries(firstSeriesId);
+
+ const series = getSeries(seriesId);
+
+ const {
+ time: { from: seriesFrom, to: seriesTo },
+ } = series;
+
+ const startDate = parseAbsoluteDate(seriesFrom ?? from)!;
+ const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!;
+
+ const onStartChange = (newDate: Moment) => {
+ if (reportType === 'kpi-over-time') {
+ const mainStartDate = parseAbsoluteDate(from)!;
+ const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
+ const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
+ const newFrom = newDate.toISOString();
+ const newTo = newDate.add(totalDuration, 'millisecond').toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: newFrom, to: newTo },
+ });
+ } else {
+ const newFrom = newDate.toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: newFrom, to: seriesTo },
+ });
+ }
+ };
+ const onEndChange = (newDate: Moment) => {
+ if (reportType === 'kpi-over-time') {
+ const mainStartDate = parseAbsoluteDate(from)!;
+ const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
+ const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
+ const newTo = newDate.toISOString();
+ const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: newFrom, to: newTo },
+ });
+ } else {
+ const newTo = newDate.toISOString();
+
+ setSeries(seriesId, {
+ ...series,
+ time: { from: seriesFrom, to: newTo },
+ });
+ }
+ };
+
+ return (
+ endDate}
+ aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', {
+ defaultMessage: 'Start date',
+ })}
+ dateFormat={dateFormat}
+ showTimeSelect
+ />
+ }
+ endDateControl={
+ endDate}
+ aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', {
+ defaultMessage: 'End date',
+ })}
+ dateFormat={dateFormat}
+ showTimeSelect
+ />
+ }
+ />
+ );
+}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
index d6a70532f4257..e21da424b58c8 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx
@@ -43,7 +43,7 @@ export function SeriesDatePicker({ seriesId }: Props) {
if (!series || !series.time) {
setSeries(seriesId, { ...series, time: DEFAULT_TIME });
}
- }, [seriesId, series, setSeries]);
+ }, [series, seriesId, setSeries]);
return (
-
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
index a78f6adeca39f..0f0cec0fbfcff 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx
@@ -41,8 +41,6 @@ export function FilterExpanded({
isNegated,
filters: defaultFilters,
}: Props) {
- const { indexPattern } = useAppIndexPatternContext();
-
const [value, setValue] = useState('');
const [isOpen, setIsOpen] = useState({ value: '', negate: false });
@@ -53,23 +51,25 @@ export function FilterExpanded({
const queryFilters: ESFilter[] = [];
+ const { indexPatterns } = useAppIndexPatternContext(series.dataType);
+
defaultFilters?.forEach((qFilter: PersistableFilter | ExistsFilter) => {
if (qFilter.query) {
queryFilters.push(qFilter.query);
}
const asExistFilter = qFilter as ExistsFilter;
if (asExistFilter?.exists) {
- queryFilters.push(asExistFilter.exists as QueryDslQueryContainer);
+ queryFilters.push({ exists: asExistFilter.exists } as QueryDslQueryContainer);
}
});
const { values, loading } = useValuesList({
query: value,
- indexPatternTitle: indexPattern?.title,
sourceField: field,
time: series.time,
keepHistory: true,
filters: queryFilters,
+ indexPatternTitle: indexPatterns[series.dataType]?.title,
});
const filters = series?.filters ?? [];
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
index 79eb858b7624b..c1790fea8c0c4 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx
@@ -139,7 +139,7 @@ describe('FilterValueButton', function () {
/>
);
- expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toBeCalledWith(
expect.objectContaining({
filters: [
@@ -170,7 +170,7 @@ describe('FilterValueButton', function () {
/>
);
- expect(spy).toHaveBeenCalledTimes(2);
+ expect(spy).toHaveBeenCalledTimes(6);
expect(spy).toBeCalledWith(
expect.objectContaining({
filters: [
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
index f04295a90e475..bf4ca6eb83d94 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx
@@ -41,7 +41,7 @@ export function FilterValueButton({
const series = getSeries(seriesId);
- const { indexPattern } = useAppIndexPatternContext();
+ const { indexPatterns } = useAppIndexPatternContext(series.dataType);
const { setFilter, removeFilter } = useSeriesFilters({ seriesId });
@@ -96,7 +96,6 @@ export function FilterValueButton({
) : (
button
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
index dc84352ff3b3d..e75f308dab1e5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx
@@ -26,9 +26,9 @@ export function RemoveSeries({ seriesId }: Props) {
defaultMessage: 'Click to remove series',
})}
iconType="cross"
- color="primary"
+ color="danger"
onClick={onClick}
- size="m"
+ size="s"
/>
);
}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
index 086a1d4341bbc..51ebe6c6bd9d5 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx
@@ -8,33 +8,93 @@
import React from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { isEmpty } from 'lodash';
import { RemoveSeries } from './remove_series';
-import { NEW_SERIES_KEY, useSeriesStorage } from '../../hooks/use_series_storage';
+import { useSeriesStorage } from '../../hooks/use_series_storage';
+import { SeriesUrl } from '../../types';
interface Props {
seriesId: string;
+ editorMode?: boolean;
}
-export function SeriesActions({ seriesId }: Props) {
- const { getSeries, removeSeries, setSeries } = useSeriesStorage();
+export function SeriesActions({ seriesId, editorMode = false }: Props) {
+ const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage();
const series = getSeries(seriesId);
const onEdit = () => {
- removeSeries(seriesId);
- setSeries(NEW_SERIES_KEY, { ...series });
+ setSeries(seriesId, { ...series, isNew: true });
+ };
+
+ const copySeries = () => {
+ let copySeriesId: string = `${seriesId}-copy`;
+ if (allSeriesIds.includes(copySeriesId)) {
+ copySeriesId = copySeriesId + allSeriesIds.length;
+ }
+ setSeries(copySeriesId, series);
+ };
+
+ const { reportType, reportDefinitions, isNew, ...restSeries } = series;
+ const isSaveAble = reportType && !isEmpty(reportDefinitions);
+
+ const saveSeries = () => {
+ if (isSaveAble) {
+ const reportDefId = Object.values(reportDefinitions ?? {})[0];
+ let newSeriesId = `${reportDefId}-${reportType}`;
+
+ if (allSeriesIds.includes(newSeriesId)) {
+ newSeriesId = `${newSeriesId}-${allSeriesIds.length}`;
+ }
+ const newSeriesN: SeriesUrl = {
+ ...restSeries,
+ reportType,
+ reportDefinitions,
+ };
+
+ setSeries(newSeriesId, newSeriesN);
+ removeSeries(seriesId);
+ }
};
return (
-
-
-
-
+
+ {!editorMode && (
+
+
+
+ )}
+ {editorMode && (
+
+
+
+ )}
+ {editorMode && (
+
+
+
+ )}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
index 8363b6b0eadfd..61081e7cc6f46 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx
@@ -16,7 +16,7 @@ describe('SelectedFilters', function () {
mockAppIndexPattern();
const dataViewSeries = getDefaultConfigs({
- reportType: 'dist',
+ reportType: 'data-distribution',
indexPattern: mockIndexPattern,
dataType: 'ux',
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
index 63abb581c9c72..33496e617a3a6 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx
@@ -39,7 +39,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
const { removeFilter } = useSeriesFilters({ seriesId });
- const { indexPattern } = useAppIndexPatternContext();
+ const { indexPattern } = useAppIndexPatternContext(series.dataType);
return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? (
@@ -55,6 +55,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
value={val}
removeFilter={() => removeFilter({ field, value: val, negate: false })}
negate={false}
+ indexPattern={indexPattern}
/>
))}
@@ -67,6 +68,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
value={val}
negate={true}
removeFilter={() => removeFilter({ field, value: val, negate: true })}
+ indexPattern={indexPattern}
/>
))}
@@ -87,6 +89,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
}}
negate={false}
definitionFilter={true}
+ indexPattern={indexPattern}
/>
))}
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
index 17d4356dcf65b..bcceeb204a31e 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx
@@ -24,7 +24,7 @@ interface EditItem {
}
export function SeriesEditor() {
- const { allSeries, firstSeriesId } = useSeriesStorage();
+ const { allSeries, allSeriesIds } = useSeriesStorage();
const columns = [
{
@@ -33,80 +33,77 @@ export function SeriesEditor() {
}),
field: 'id',
width: '15%',
- render: (val: string) => (
+ render: (seriesId: string) => (
{' '}
- {val === NEW_SERIES_KEY ? 'series-preview' : val}
+ {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId}
),
},
- ...(firstSeriesId !== NEW_SERIES_KEY
- ? [
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
- defaultMessage: 'Filters',
- }),
- field: 'defaultFilters',
- width: '15%',
- render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => (
-
- ),
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
- defaultMessage: 'Breakdowns',
- }),
- field: 'breakdowns',
- width: '25%',
- render: (val: string[], item: EditItem) => (
-
- ),
- },
- {
- name: (
-
-
-
- ),
- width: '20%',
- field: 'id',
- align: 'right' as const,
- render: (val: string, item: EditItem) => ,
- },
- {
- name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
- defaultMessage: 'Actions',
- }),
- align: 'center' as const,
- width: '10%',
- field: 'id',
- render: (val: string, item: EditItem) => ,
- },
- ]
- : []),
+ {
+ name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
+ defaultMessage: 'Filters',
+ }),
+ field: 'defaultFilters',
+ width: '15%',
+ render: (seriesId: string, { seriesConfig, id }: EditItem) => (
+
+ ),
+ },
+ {
+ name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
+ defaultMessage: 'Breakdowns',
+ }),
+ field: 'id',
+ width: '25%',
+ render: (seriesId: string, { seriesConfig, id }: EditItem) => (
+
+ ),
+ },
+ {
+ name: (
+
+
+
+ ),
+ width: '20%',
+ field: 'id',
+ align: 'right' as const,
+ render: (seriesId: string, item: EditItem) => ,
+ },
+ {
+ name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
+ defaultMessage: 'Actions',
+ }),
+ align: 'center' as const,
+ width: '10%',
+ field: 'id',
+ render: (seriesId: string, item: EditItem) => ,
+ },
];
- const allSeriesKeys = Object.keys(allSeries);
-
+ const { indexPatterns } = useAppIndexPatternContext();
const items: EditItem[] = [];
- const { indexPattern } = useAppIndexPatternContext();
-
- allSeriesKeys.forEach((seriesKey) => {
+ allSeriesIds.forEach((seriesKey) => {
const series = allSeries[seriesKey];
- if (series.reportType && indexPattern) {
+ if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) {
items.push({
id: seriesKey,
seriesConfig: getDefaultConfigs({
- indexPattern,
+ indexPattern: indexPatterns[series.dataType],
reportType: series.reportType,
dataType: series.dataType,
}),
@@ -114,6 +111,10 @@ export function SeriesEditor() {
}
});
+ if (items.length === 0 && allSeriesIds.length > 0) {
+ return null;
+ }
+
return (
<>
@@ -121,8 +122,7 @@ export function SeriesEditor() {
items={items}
rowHeader="firstName"
columns={columns}
- rowProps={() => (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })}
- noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
+ noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', {
defaultMessage: 'No series found, please add a series.',
})}
cellProps={{
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
index 73b4d7794dd51..e8fccc5baab34 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
@@ -23,7 +23,7 @@ export const ReportViewTypes = {
dist: 'data-distribution',
kpi: 'kpi-over-time',
cwv: 'core-web-vitals',
- mdd: 'mobile-device-distribution',
+ mdd: 'device-data-distribution',
} as const;
type ValueOf = T[keyof T];
@@ -56,7 +56,6 @@ export interface DataSeries {
reportType: ReportViewType;
xAxisColumn: Partial | Partial;
yAxisColumns: Array>;
-
breakdowns: string[];
defaultSeriesType: SeriesType;
defaultFilters: Array;
@@ -80,10 +79,11 @@ export interface SeriesUrl {
breakdown?: string;
filters?: UrlFilter[];
seriesType?: SeriesType;
- reportType: ReportViewTypeId;
+ reportType: ReportViewType;
operationType?: OperationType;
dataType: AppDataType;
reportDefinitions?: URLReportDefinition;
+ isNew?: boolean;
}
export interface UrlFilter {
@@ -94,6 +94,7 @@ export interface UrlFilter {
export interface ConfigProps {
indexPattern: IIndexPattern;
+ series?: SeriesUrl;
}
export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts
new file mode 100644
index 0000000000000..fe545fff5498d
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { urlFiltersToKueryString } from './stringify_kueries';
+import { UrlFilter } from '../types';
+import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
+
+describe('stringifyKueries', () => {
+ let filters: UrlFilter[];
+ beforeEach(() => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome', 'Firefox'],
+ notValues: [],
+ },
+ ];
+ });
+
+ it('stringifies the current values', () => {
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\")"`
+ );
+ });
+
+ it('correctly stringifies a single value', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\")"`
+ );
+ });
+
+ it('returns an empty string for an empty array', () => {
+ expect(urlFiltersToKueryString([])).toMatchInlineSnapshot(`""`);
+ });
+
+ it('returns an empty string for an empty value', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(`""`);
+ });
+
+ it('adds quotations if the value contains a space', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Google Chrome'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Google Chrome\\")"`
+ );
+ });
+
+ it('adds quotations inside parens if there are values containing spaces', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Google Chrome'],
+ notValues: ['Apple Safari'],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Google Chrome\\") and not (user_agent.name: (\\"Apple Safari\\"))"`
+ );
+ });
+
+ it('handles parens for values with greater than 2 items', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome', 'Firefox', 'Safari', 'Opera'],
+ notValues: ['Safari'],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\" or \\"Safari\\" or \\"Opera\\") and not (user_agent.name: (\\"Safari\\"))"`
+ );
+ });
+
+ it('handles colon characters in values', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+
+ it('handles precending empty array', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+
+ it('handles skipped empty arrays', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ },
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\") and url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts
new file mode 100644
index 0000000000000..8a92c724338ef
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { UrlFilter } from '../types';
+
+/**
+ * Extract a map's keys to an array, then map those keys to a string per key.
+ * The strings contain all of the values chosen for the given field (which is also the key value).
+ * Reduce the list of query strings to a singular string, with AND operators between.
+ */
+export const urlFiltersToKueryString = (urlFilters: UrlFilter[]): string => {
+ let kueryString = '';
+ urlFilters.forEach(({ field, values, notValues }) => {
+ const valuesT = values?.map((val) => `"${val}"`);
+ const notValuesT = notValues?.map((val) => `"${val}"`);
+
+ if (valuesT && valuesT?.length > 0) {
+ if (kueryString.length > 0) {
+ kueryString += ' and ';
+ }
+ kueryString += `${field}: (${valuesT.join(' or ')})`;
+ }
+
+ if (notValuesT && notValuesT?.length > 0) {
+ if (kueryString.length > 0) {
+ kueryString += ' and ';
+ }
+ kueryString += `not (${field}: (${notValuesT.join(' or ')}))`;
+ }
+ });
+
+ return kueryString;
+};
diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
index 5a7ce3502ce84..896aca79114d7 100644
--- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
+++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiSideNavItemType, ExclusiveUnion } from '@elastic/eui';
+import { EuiSideNavItemType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
@@ -28,13 +28,9 @@ export type WrappedPageTemplateProps = Pick<
| 'pageContentProps'
| 'pageHeader'
| 'restrictWidth'
+ | 'template'
| 'isEmptyState'
-> &
- // recreate the exclusivity of bottomBar-related props
- ExclusiveUnion<
- { template?: 'default' } & Pick,
- { template: KibanaPageTemplateProps['template'] }
- >;
+>;
export interface ObservabilityPageTemplateDependencies {
currentAppId$: Observable;
diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx
index 92f51aeff9bd6..f97e3fb996441 100644
--- a/x-pack/plugins/observability/public/routes/index.tsx
+++ b/x-pack/plugins/observability/public/routes/index.tsx
@@ -112,4 +112,18 @@ export const routes = {
}),
},
},
+ // enable this to test multi series architecture
+ // '/exploratory-view/multi': {
+ // handler: () => {
+ // return ;
+ // },
+ // params: {
+ // query: t.partial({
+ // rangeFrom: t.string,
+ // rangeTo: t.string,
+ // refreshPaused: jsonRt.pipe(t.boolean),
+ // refreshInterval: jsonRt.pipe(t.number),
+ // }),
+ // },
+ // },
};
diff --git a/x-pack/plugins/osquery/cypress/README.md b/x-pack/plugins/osquery/cypress/README.md
new file mode 100644
index 0000000000000..0df311ebc0a05
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/README.md
@@ -0,0 +1,138 @@
+# Cypress Tests
+
+The `osquery/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/).
+
+## Running the tests
+
+There are currently three ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below.
+
+### Execution modes
+
+#### Interactive mode
+
+When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview).
+
+#### Headless mode
+
+A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser, but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished.
+
+### Target environments
+
+#### FTR (CI)
+
+This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/security_solution_cypress`
+
+### Test Execution: Examples
+
+#### FTR + Headless (Chrome)
+
+Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc.
+
+```shell
+# bootstrap kibana from the project root
+yarn kbn bootstrap
+
+# build the plugins/assets that cypress will execute against
+node scripts/build_kibana_platform_plugins
+
+# launch the cypress test runner
+cd x-pack/plugins/security_solution
+yarn cypress:run-as-ci
+```
+#### FTR + Interactive
+
+This is the preferred mode for developing new tests.
+
+```shell
+# bootstrap kibana from the project root
+yarn kbn bootstrap
+
+# build the plugins/assets that cypress will execute against
+node scripts/build_kibana_platform_plugins
+
+# launch the cypress test runner
+cd x-pack/plugins/security_solution
+yarn cypress:open-as-ci
+```
+
+Note that you can select the browser you want to use on the top right side of the interactive runner.
+
+## Folder Structure
+
+### integration/
+
+Cypress convention. Contains the specs that are going to be executed.
+
+### fixtures/
+
+Cypress convention. Fixtures are used as external pieces of static data when we stub responses.
+
+### plugins/
+
+Cypress convention. As a convenience, by default Cypress will automatically include the plugins file cypress/plugins/index.js before every single spec file it runs.
+
+### screens/
+
+Contains the elements we want to interact with in our tests.
+
+Each file inside the screens folder represents a screen in our application.
+
+### tasks/
+
+_Tasks_ are functions that may be reused across tests.
+
+Each file inside the tasks folder represents a screen of our application.
+
+## Test data
+
+The data the tests need:
+
+- Is generated on the fly using our application APIs (preferred way)
+- Is ingested on the ELS instance using the `es_archive` utility
+
+### How to generate a new archive
+
+**Note:** As mentioned above, archives are only meant to contain external data, e.g. beats data. Due to the tendency for archived domain objects (rules, signals) to quickly become out of date, it is strongly suggested that you generate this data within the test, through interaction with either the UI or the API.
+
+We use es_archiver to manage the data that our Cypress tests need.
+
+1. Set up a clean instance of kibana and elasticsearch (if this is not possible, try to clean/minimize the data that you are going to archive).
+2. With the kibana and elasticsearch instance up and running, create the data that you need for your test.
+3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution`
+
+```sh
+node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@:
+```
+
+Example:
+
+```sh
+node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220
+```
+
+Note that the command will create the folder if it does not exist.
+
+## Development Best Practices
+
+### Clean up the state
+
+Remember to clean up the state of the test after its execution, typically with the `cleanKibana` function. Be mindful of failure scenarios, as well: if your test fails, will it leave the environment in a recoverable state?
+
+### Minimize the use of es_archive
+
+When possible, create all the data that you need for executing the tests using the application APIS or the UI.
+
+### Speed up test execution time
+
+Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be
+taken into consideration until another solution is implemented:
+
+- Group the tests that are similar in different contexts.
+- For every context login only once, clean the state between tests if needed without re-loading the page.
+- All tests in a spec file must be order-independent.
+
+Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time.
+
+## Linting
+
+Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage)
diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json
new file mode 100644
index 0000000000000..eb24616607ec3
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/cypress.json
@@ -0,0 +1,14 @@
+{
+ "baseUrl": "http://localhost:5620",
+ "defaultCommandTimeout": 60000,
+ "execTimeout": 120000,
+ "pageLoadTimeout": 120000,
+ "nodeVersion": "system",
+ "retries": {
+ "runMode": 2
+ },
+ "trashAssetsBeforeRuns": false,
+ "video": false,
+ "viewportHeight": 900,
+ "viewportWidth": 1440
+}
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts
new file mode 100644
index 0000000000000..0babfd2f10a8e
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { HEADER } from '../screens/osquery';
+import { OSQUERY_NAVIGATION_LINK } from '../screens/navigation';
+
+import { INTEGRATIONS, OSQUERY, openNavigationFlyout, navigateTo } from '../tasks/navigation';
+import { addIntegration } from '../tasks/integrations';
+
+describe('Osquery Manager', () => {
+ before(() => {
+ navigateTo(INTEGRATIONS);
+ addIntegration('Osquery Manager');
+ });
+
+ it('Displays Osquery on the navigation flyout once installed ', () => {
+ openNavigationFlyout();
+ cy.get(OSQUERY_NAVIGATION_LINK).should('exist');
+ });
+
+ it('Displays Live queries history title when navigating to Osquery', () => {
+ navigateTo(OSQUERY);
+ cy.get(HEADER).should('have.text', 'Live queries history');
+ });
+});
diff --git a/x-pack/plugins/osquery/cypress/plugins/index.js b/x-pack/plugins/osquery/cypress/plugins/index.js
new file mode 100644
index 0000000000000..7dbb69ced7016
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/plugins/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+module.exports = (_on, _config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+};
diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts
new file mode 100644
index 0000000000000..0b29e857f46ee
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/integrations.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const ADD_POLICY_BTN = '[data-test-subj="addIntegrationPolicyButton"]';
+export const CREATE_PACKAGE_POLICY_SAVE_BTN = '[data-test-subj="createPackagePolicySaveButton"]';
+export const INTEGRATIONS_CARD = '.euiCard__titleAnchor';
diff --git a/x-pack/plugins/osquery/cypress/screens/navigation.ts b/x-pack/plugins/osquery/cypress/screens/navigation.ts
new file mode 100644
index 0000000000000..7884cf347d7c0
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/navigation.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]';
+export const OSQUERY_NAVIGATION_LINK = '[data-test-subj="collapsibleNavAppLink"] [title="Osquery"]';
diff --git a/x-pack/plugins/osquery/cypress/screens/osquery.ts b/x-pack/plugins/osquery/cypress/screens/osquery.ts
new file mode 100644
index 0000000000000..bc387a57e9e3c
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/osquery.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const HEADER = 'h1';
diff --git a/x-pack/plugins/osquery/cypress/support/commands.js b/x-pack/plugins/osquery/cypress/support/commands.js
new file mode 100644
index 0000000000000..66f9435035571
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/support/commands.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/index.ts
new file mode 100644
index 0000000000000..72618c943f4d2
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/support/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands';
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+Cypress.on('uncaught:exception', () => {
+ return false;
+});
diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts
new file mode 100644
index 0000000000000..f85ef56550af5
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tasks/integrations.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ADD_POLICY_BTN,
+ CREATE_PACKAGE_POLICY_SAVE_BTN,
+ INTEGRATIONS_CARD,
+} from '../screens/integrations';
+
+export const addIntegration = (integration: string) => {
+ cy.get(INTEGRATIONS_CARD).contains(integration).click();
+ cy.get(ADD_POLICY_BTN).click();
+ cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).click();
+ cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).should('not.exist');
+ cy.reload();
+};
diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts
new file mode 100644
index 0000000000000..63d6b205b433b
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tasks/navigation.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation';
+
+export const INTEGRATIONS = 'app/integrations#/';
+export const OSQUERY = 'app/osquery/live_queries';
+
+export const navigateTo = (page: string) => {
+ cy.visit(page);
+};
+
+export const openNavigationFlyout = () => {
+ cy.get(TOGGLE_NAVIGATION_BTN).click();
+};
diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json
new file mode 100644
index 0000000000000..467ea13fc4869
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "exclude": [],
+ "include": [
+ "./**/*"
+ ],
+ "compilerOptions": {
+ "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress",
+ "types": [
+ "cypress",
+ "node"
+ ],
+ "resolveJsonModule": true,
+ },
+ }
diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json
new file mode 100644
index 0000000000000..5bbb95e556d6b
--- /dev/null
+++ b/x-pack/plugins/osquery/package.json
@@ -0,0 +1,13 @@
+{
+ "author": "Elastic",
+ "name": "osquery",
+ "version": "8.0.0",
+ "private": true,
+ "license": "Elastic-License",
+ "scripts": {
+ "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json",
+ "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts",
+ "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json",
+ "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts"
+ }
+}
diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts
index 6a4236b5adccd..3d5f3592101fd 100644
--- a/x-pack/plugins/osquery/server/usage/fetchers.ts
+++ b/x-pack/plugins/osquery/server/usage/fetchers.ts
@@ -56,6 +56,7 @@ export async function getPolicyLevelUsage(
},
},
index: '.fleet-agents',
+ ignore_unavailable: true,
});
const policied = agentResponse.body.aggregations?.policied as AggregationsSingleBucketAggregate;
if (policied && typeof policied.doc_count === 'number') {
@@ -118,6 +119,7 @@ export async function getLiveQueryUsage(
},
},
index: '.fleet-actions',
+ ignore_unavailable: true,
});
const result: LiveQueryUsage = {
session: await getRouteMetric(soClient, 'live_query'),
@@ -226,6 +228,7 @@ export async function getBeatUsage(esClient: ElasticsearchClient) {
},
},
index: METRICS_INDICES,
+ ignore_unavailable: true,
});
return extractBeatUsageMetrics(metricResponse);
diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts
index 2148cf983d889..8205b4f13a320 100644
--- a/x-pack/plugins/reporting/common/types.ts
+++ b/x-pack/plugins/reporting/common/types.ts
@@ -68,6 +68,7 @@ export interface ReportSource {
};
meta: { objectType: string; layout?: string };
browser_type: string;
+ migration_version: string;
max_attempts: number;
timeout: number;
@@ -77,7 +78,7 @@ export interface ReportSource {
started_at?: string;
completed_at?: string;
created_at: string;
- process_expiration?: string;
+ process_expiration?: string | null; // must be set to null to clear the expiration
}
/*
diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
index a6753211fba3b..01a8be98bc4be 100644
--- a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
+++ b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
@@ -64,7 +64,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout
className="euiFormRow__fieldWrapper"
>
-
-
-
- }
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- Unable to fetch report info
-
-
-
-
-
-
-
+
-
-
- Could not fetch the job info
-
-
+ Could not fetch the job info
-
+
-
+
-
-
+
+
,
-
-
-
-
-
-
-
-
-
+
@@ -215,6 +122,7 @@ Array [
>
-
-
-
- }
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- Job Info
-
-
-
-
-
-
-
+ className="euiText euiText--medium"
+ />
+
-
+
-
-
+
+
,
-
-
-
-
-
-
-
-
-
+
@@ -420,6 +235,7 @@ Array [
>
= {
JOB_STATUS_PENDING: 'pending',
JOB_STATUS_PROCESSING: 'processing',
JOB_STATUS_COMPLETED: 'completed',
JOB_STATUS_WARNINGS: 'completed_with_warnings',
JOB_STATUS_FAILED: 'failed',
- JOB_STATUS_CANCELLED: 'cancelled',
};
diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts
index ce8f768ef077f..69f432562ec98 100644
--- a/x-pack/plugins/reporting/server/lib/store/mapping.ts
+++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts
@@ -7,15 +7,10 @@
export const mapping = {
meta: {
- // We are indexing these properties with both text and keyword fields because that's what will be auto generated
- // when an index already exists. This schema is only used when a reporting index doesn't exist. This way existing
- // reporting indexes and new reporting indexes will look the same and the data can be queried in the same
- // manner.
+ // We are indexing these properties with both text and keyword fields
+ // because that's what will be auto generated when an index already exists.
properties: {
- /**
- * Type of object that is triggering this report. Should be either search, visualization or dashboard.
- * Used for job listing and telemetry stats only.
- */
+ // ID of the app this report: search, visualization or dashboard, etc
objectType: {
type: 'text',
fields: {
@@ -25,10 +20,6 @@ export const mapping = {
},
},
},
- /**
- * Can be either preserve_layout, print or none (in the case of csv export).
- * Used for phone home stats only.
- */
layout: {
type: 'text',
fields: {
@@ -41,9 +32,10 @@ export const mapping = {
},
},
browser_type: { type: 'keyword' },
+ migration_version: { type: 'keyword' }, // new field (7.14) to distinguish reports that were scheduled with Task Manager
jobtype: { type: 'keyword' },
payload: { type: 'object', enabled: false },
- priority: { type: 'byte' }, // NOTE: this is unused, but older data may have a mapping for this field
+ priority: { type: 'byte' }, // TODO: remove: this is unused
timeout: { type: 'long' },
process_expiration: { type: 'date' },
created_by: { type: 'keyword' }, // `null` if security is disabled
diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts
index 23d766f2190f6..a8d14e12a738b 100644
--- a/x-pack/plugins/reporting/server/lib/store/report.test.ts
+++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts
@@ -20,21 +20,18 @@ describe('Class Report', () => {
timeout: 30000,
});
- expect(report.toEsDocsJSON()).toMatchObject({
- _index: '.reporting-test-index-12345',
- _source: {
- attempts: 0,
- browser_type: 'browser_type_test_string',
- completed_at: undefined,
- created_by: 'created_by_test_string',
- jobtype: 'test-report',
- max_attempts: 50,
- meta: { objectType: 'test' },
- payload: { headers: 'payload_test_field', objectType: 'testOt' },
- started_at: undefined,
- status: 'pending',
- timeout: 30000,
- },
+ expect(report.toReportSource()).toMatchObject({
+ attempts: 0,
+ browser_type: 'browser_type_test_string',
+ completed_at: undefined,
+ created_by: 'created_by_test_string',
+ jobtype: 'test-report',
+ max_attempts: 50,
+ meta: { objectType: 'test' },
+ payload: { headers: 'payload_test_field', objectType: 'testOt' },
+ started_at: undefined,
+ status: 'pending',
+ timeout: 30000,
});
expect(report.toReportTaskJSON()).toMatchObject({
attempts: 0,
@@ -80,22 +77,18 @@ describe('Class Report', () => {
};
report.updateWithEsDoc(metadata);
- expect(report.toEsDocsJSON()).toMatchObject({
- _id: '12342p9o387549o2345',
- _index: '.reporting-test-update',
- _source: {
- attempts: 0,
- browser_type: 'browser_type_test_string',
- completed_at: undefined,
- created_by: 'created_by_test_string',
- jobtype: 'test-report',
- max_attempts: 50,
- meta: { objectType: 'stange' },
- payload: { objectType: 'testOt' },
- started_at: undefined,
- status: 'pending',
- timeout: 30000,
- },
+ expect(report.toReportSource()).toMatchObject({
+ attempts: 0,
+ browser_type: 'browser_type_test_string',
+ completed_at: undefined,
+ created_by: 'created_by_test_string',
+ jobtype: 'test-report',
+ max_attempts: 50,
+ meta: { objectType: 'stange' },
+ payload: { objectType: 'testOt' },
+ started_at: undefined,
+ status: 'pending',
+ timeout: 30000,
});
expect(report.toReportTaskJSON()).toMatchObject({
attempts: 0,
diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts
index 9b98650e1d984..fa5b91527ccc4 100644
--- a/x-pack/plugins/reporting/server/lib/store/report.ts
+++ b/x-pack/plugins/reporting/server/lib/store/report.ts
@@ -21,8 +21,13 @@ export { ReportDocument };
export { ReportApiJSON, ReportSource };
const puid = new Puid();
+export const MIGRATION_VERSION = '7.14.0';
-export class Report implements Partial
{
+/*
+ * The public fields are a flattened version what Elasticsearch returns when you
+ * `GET` a document.
+ */
+export class Report implements Partial {
public _index?: string;
public _id: string;
public _primary_term?: number; // set by ES
@@ -47,6 +52,7 @@ export class Report implements Partial {
public readonly timeout?: ReportSource['timeout'];
public process_expiration?: ReportSource['process_expiration'];
+ public migration_version: string;
/*
* Create an unsaved report
@@ -58,6 +64,8 @@ export class Report implements Partial {
this._primary_term = opts._primary_term;
this._seq_no = opts._seq_no;
+ this.migration_version = MIGRATION_VERSION;
+
this.payload = opts.payload!;
this.kibana_name = opts.kibana_name!;
this.kibana_id = opts.kibana_id!;
@@ -80,7 +88,7 @@ export class Report implements Partial {
/*
* Update the report with "live" storage metadata
*/
- updateWithEsDoc(doc: Partial) {
+ updateWithEsDoc(doc: Partial): void {
if (doc._index == null || doc._id == null) {
throw new Error(`Report object from ES has missing fields!`);
}
@@ -89,30 +97,31 @@ export class Report implements Partial {
this._index = doc._index;
this._primary_term = doc._primary_term;
this._seq_no = doc._seq_no;
+ this.migration_version = MIGRATION_VERSION;
}
/*
* Data structure for writing to Elasticsearch index
*/
- toEsDocsJSON() {
+ toReportSource(): ReportSource {
return {
- _id: this._id,
- _index: this._index,
- _source: {
- jobtype: this.jobtype,
- created_at: this.created_at,
- created_by: this.created_by,
- payload: this.payload,
- meta: this.meta,
- timeout: this.timeout,
- max_attempts: this.max_attempts,
- browser_type: this.browser_type,
- status: this.status,
- attempts: this.attempts,
- started_at: this.started_at,
- completed_at: this.completed_at,
- process_expiration: this.process_expiration,
- },
+ migration_version: MIGRATION_VERSION,
+ kibana_name: this.kibana_name,
+ kibana_id: this.kibana_id,
+ jobtype: this.jobtype,
+ created_at: this.created_at,
+ created_by: this.created_by,
+ payload: this.payload,
+ meta: this.meta,
+ timeout: this.timeout!,
+ max_attempts: this.max_attempts,
+ browser_type: this.browser_type!,
+ status: this.status,
+ attempts: this.attempts,
+ started_at: this.started_at,
+ completed_at: this.completed_at,
+ process_expiration: this.process_expiration,
+ output: this.output || null,
};
}
diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts
index 7f96433fcc6ce..8bb5c7fb8bbf9 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.test.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts
@@ -184,6 +184,7 @@ describe('ReportingStore', () => {
_source: {
kibana_name: 'test',
kibana_id: 'test123',
+ migration_version: 'X.0.0',
created_at: 'some time',
created_by: 'some security person',
jobtype: 'csv',
@@ -222,6 +223,7 @@ describe('ReportingStore', () => {
"meta": Object {
"testMeta": "meta",
},
+ "migration_version": "7.14.0",
"output": null,
"payload": Object {
"testPayload": "payload",
@@ -239,6 +241,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'id-of-processing',
_index: '.reporting-test-index-12345',
+ _seq_no: 42,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -254,24 +258,12 @@ describe('ReportingStore', () => {
await store.setReportClaimed(report, { testDoc: 'test' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "status": "processing",
- "testDoc": "test",
- },
- },
- "id": "id-of-processing",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`processing`);
+ expect(updateCall.if_seq_no).toBe(42);
+ expect(updateCall.if_primary_term).toBe(10002);
});
it('setReportFailed sets the status of a record to failed', async () => {
@@ -279,6 +271,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'id-of-failure',
_index: '.reporting-test-index-12345',
+ _seq_no: 43,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -294,24 +288,12 @@ describe('ReportingStore', () => {
await store.setReportFailed(report, { errors: 'yes' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "errors": "yes",
- "status": "failed",
- },
- },
- "id": "id-of-failure",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`failed`);
+ expect(updateCall.if_seq_no).toBe(43);
+ expect(updateCall.if_primary_term).toBe(10002);
});
it('setReportCompleted sets the status of a record to completed', async () => {
@@ -319,6 +301,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'vastly-great-report-id',
_index: '.reporting-test-index-12345',
+ _seq_no: 44,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -334,31 +318,21 @@ describe('ReportingStore', () => {
await store.setReportCompleted(report, { certainly_completed: 'yes' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "certainly_completed": "yes",
- "status": "completed",
- },
- },
- "id": "vastly-great-report-id",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`completed`);
+ expect(updateCall.if_seq_no).toBe(44);
+ expect(updateCall.if_primary_term).toBe(10002);
});
- it('setReportCompleted sets the status of a record to completed_with_warnings', async () => {
+ it('sets the status of a record to completed_with_warnings', async () => {
const store = new ReportingStore(mockCore, mockLogger);
const report = new Report({
_id: 'vastly-great-report-id',
_index: '.reporting-test-index-12345',
+ _seq_no: 45,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -379,28 +353,52 @@ describe('ReportingStore', () => {
},
} as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "certainly_completed": "pretty_much",
- "output": Object {
- "warnings": Array [
- "those pants don't go with that shirt",
- ],
- },
- "status": "completed_with_warnings",
- },
- },
- "id": "vastly-great-report-id",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`completed_with_warnings`);
+ expect(updateCall.if_seq_no).toBe(45);
+ expect(updateCall.if_primary_term).toBe(10002);
+ expect(response.output).toMatchInlineSnapshot(`
+ Object {
+ "warnings": Array [
+ "those pants don't go with that shirt",
+ ],
+ }
`);
});
+
+ it('prepareReportForRetry resets the expiration and status on the report document', async () => {
+ const store = new ReportingStore(mockCore, mockLogger);
+ const report = new Report({
+ _id: 'pretty-good-report-id',
+ _index: '.reporting-test-index-94058763',
+ _seq_no: 46,
+ _primary_term: 10002,
+ jobtype: 'test-report-2',
+ created_by: 'created_by_test_string',
+ browser_type: 'browser_type_test_string',
+ status: 'processing',
+ process_expiration: '2002',
+ max_attempts: 3,
+ payload: {
+ title: 'test report',
+ headers: 'rp_test_headers',
+ objectType: 'testOt',
+ browserTimezone: 'utc',
+ },
+ timeout: 30000,
+ });
+
+ await store.prepareReportForRetry(report);
+
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`pending`);
+ expect(updateCall.if_seq_no).toBe(46);
+ expect(updateCall.if_primary_term).toBe(10002);
+ });
});
diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts
index fc7bd9c23d769..8f1e6c315a2d1 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.ts
@@ -5,15 +5,38 @@
* 2.0.
*/
+import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/api/types';
import { ElasticsearchClient } from 'src/core/server';
import { LevelLogger, statuses } from '../';
import { ReportingCore } from '../../';
-import { numberToDuration } from '../../../common/schema_utils';
import { JobStatus } from '../../../common/types';
import { ReportTaskParams } from '../tasks';
import { indexTimestamp } from './index_timestamp';
import { mapping } from './mapping';
-import { Report, ReportDocument, ReportSource } from './report';
+import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report';
+
+/*
+ * When an instance of Kibana claims a report job, this information tells us about that instance
+ */
+export type ReportProcessingFields = Required<{
+ kibana_id: Report['kibana_id'];
+ kibana_name: Report['kibana_name'];
+ browser_type: Report['browser_type'];
+ attempts: Report['attempts'];
+ started_at: Report['started_at'];
+ timeout: Report['timeout'];
+ process_expiration: Report['process_expiration'];
+}>;
+
+export type ReportFailedFields = Required<{
+ completed_at: Report['completed_at'];
+ output: Report['output'];
+}>;
+
+export type ReportCompletedFields = Required<{
+ completed_at: Report['completed_at'];
+ output: Report['output'];
+}>;
/*
* When searching for long-pending reports, we get a subset of fields
@@ -24,15 +47,38 @@ export interface ReportRecordTimeout {
_source: {
status: JobStatus;
process_expiration?: string;
- created_at?: string;
};
}
const checkReportIsEditable = (report: Report) => {
- if (!report._id || !report._index) {
- throw new Error(`Report object is not synced with ES!`);
+ const { _id, _index, _seq_no, _primary_term } = report;
+ if (_id == null || _index == null) {
+ throw new Error(`Report is not editable: Job [${_id}] is not synced with ES!`);
+ }
+
+ if (_seq_no == null || _primary_term == null) {
+ throw new Error(
+ `Report is not editable: Job [${_id}] is missing _seq_no and _primary_term fields!`
+ );
}
};
+/*
+ * When searching for long-pending reports, we get a subset of fields
+ */
+const sourceDoc = (doc: Partial): Partial => {
+ return {
+ ...doc,
+ migration_version: MIGRATION_VERSION,
+ };
+};
+
+const jobDebugMessage = (report: Report) =>
+ `${report._id} ` +
+ `[_index: ${report._index}] ` +
+ `[_seq_no: ${report._seq_no}] ` +
+ `[_primary_term: ${report._primary_term}]` +
+ `[attempts: ${report.attempts}] ` +
+ `[process_expiration: ${report.process_expiration}]`;
/*
* A class to give an interface to historical reports in the reporting.index
@@ -43,7 +89,6 @@ const checkReportIsEditable = (report: Report) => {
export class ReportingStore {
private readonly indexPrefix: string; // config setting of index prefix in system index name
private readonly indexInterval: string; // config setting of index prefix: how often to poll for pending work
- private readonly queueTimeoutMins: number; // config setting of queue timeout, rounded up to nearest minute
private client?: ElasticsearchClient;
constructor(private reportingCore: ReportingCore, private logger: LevelLogger) {
@@ -52,7 +97,6 @@ export class ReportingStore {
this.indexPrefix = config.get('index');
this.indexInterval = config.get('queue', 'indexInterval');
this.logger = logger.clone(['store']);
- this.queueTimeoutMins = Math.ceil(numberToDuration(config.get('queue', 'timeout')).asMinutes());
}
private async getClient() {
@@ -103,18 +147,20 @@ export class ReportingStore {
/*
* Called from addReport, which handles any errors
*/
- private async indexReport(report: Report) {
+ private async indexReport(report: Report): Promise {
const doc = {
index: report._index!,
id: report._id,
+ refresh: true,
body: {
- ...report.toEsDocsJSON()._source,
- process_expiration: new Date(0), // use epoch so the job query works
- attempts: 0,
- status: statuses.JOB_STATUS_PENDING,
+ ...report.toReportSource(),
+ ...sourceDoc({
+ process_expiration: new Date(0).toISOString(),
+ attempts: 0,
+ status: statuses.JOB_STATUS_PENDING,
+ }),
},
};
-
const client = await this.getClient();
const { body } = await client.index(doc);
@@ -140,8 +186,7 @@ export class ReportingStore {
await this.createIndex(index);
try {
- const doc = await this.indexReport(report);
- report.updateWithEsDoc(doc);
+ report.updateWithEsDoc(await this.indexReport(report));
await this.refreshIndex(index);
@@ -156,7 +201,9 @@ export class ReportingStore {
/*
* Search for a report from task data and return back the report
*/
- public async findReportFromTask(taskJson: ReportTaskParams): Promise {
+ public async findReportFromTask(
+ taskJson: Pick
+ ): Promise {
if (!taskJson.index) {
throw new Error('Task JSON is missing index field!');
}
@@ -186,41 +233,23 @@ export class ReportingStore {
timeout: document._source?.timeout,
});
} catch (err) {
- this.logger.error('Error in finding a report! ' + JSON.stringify({ report: taskJson }));
- this.logger.error(err);
- throw err;
- }
- }
-
- public async setReportPending(report: Report) {
- const doc = { status: statuses.JOB_STATUS_PENDING };
-
- try {
- checkReportIsEditable(report);
-
- const client = await this.getClient();
- const { body } = await client.update({
- id: report._id,
- index: report._index!,
- if_seq_no: report._seq_no,
- if_primary_term: report._primary_term,
- refresh: true,
- body: { doc },
- });
-
- return (body as unknown) as ReportDocument;
- } catch (err) {
- this.logger.error('Error in setting report pending status!');
+ this.logger.error(
+ `Error in finding the report from the scheduled task info! ` +
+ `[id: ${taskJson.id}] [index: ${taskJson.index}]`
+ );
this.logger.error(err);
throw err;
}
}
- public async setReportClaimed(report: Report, stats: Partial): Promise {
- const doc = {
- ...stats,
+ public async setReportClaimed(
+ report: Report,
+ processingInfo: ReportProcessingFields
+ ): Promise> {
+ const doc = sourceDoc({
+ ...processingInfo,
status: statuses.JOB_STATUS_PROCESSING,
- };
+ });
try {
checkReportIsEditable(report);
@@ -235,19 +264,24 @@ export class ReportingStore {
body: { doc },
});
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report processing status!');
+ this.logger.error(
+ `Error in updating status to processing! Report: ` + jobDebugMessage(report)
+ );
this.logger.error(err);
throw err;
}
}
- public async setReportFailed(report: Report, stats: Partial): Promise {
- const doc = {
- ...stats,
+ public async setReportFailed(
+ report: Report,
+ failedInfo: ReportFailedFields
+ ): Promise> {
+ const doc = sourceDoc({
+ ...failedInfo,
status: statuses.JOB_STATUS_FAILED,
- };
+ });
try {
checkReportIsEditable(report);
@@ -261,26 +295,29 @@ export class ReportingStore {
refresh: true,
body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report failed status!');
+ this.logger.error(`Error in updating status to failed! Report: ` + jobDebugMessage(report));
this.logger.error(err);
throw err;
}
}
- public async setReportCompleted(report: Report, stats: Partial): Promise {
+ public async setReportCompleted(
+ report: Report,
+ completedInfo: ReportCompletedFields
+ ): Promise> {
+ const { output } = completedInfo;
+ const status =
+ output && output.warnings && output.warnings.length > 0
+ ? statuses.JOB_STATUS_WARNINGS
+ : statuses.JOB_STATUS_COMPLETED;
+ const doc = sourceDoc({
+ ...completedInfo,
+ status,
+ });
+
try {
- const { output } = stats;
- const status =
- output && output.warnings && output.warnings.length > 0
- ? statuses.JOB_STATUS_WARNINGS
- : statuses.JOB_STATUS_COMPLETED;
- const doc = {
- ...stats,
- status,
- };
checkReportIsEditable(report);
const client = await this.getClient();
@@ -292,16 +329,20 @@ export class ReportingStore {
refresh: true,
body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report complete status!');
+ this.logger.error(`Error in updating status to complete! Report: ` + jobDebugMessage(report));
this.logger.error(err);
throw err;
}
}
- public async clearExpiration(report: Report): Promise {
+ public async prepareReportForRetry(report: Report): Promise> {
+ const doc = sourceDoc({
+ status: statuses.JOB_STATUS_PENDING,
+ process_expiration: null,
+ });
+
try {
checkReportIsEditable(report);
@@ -312,50 +353,54 @@ export class ReportingStore {
if_seq_no: report._seq_no,
if_primary_term: report._primary_term,
refresh: true,
- body: { doc: { process_expiration: null } },
+ body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in clearing expiration!');
+ this.logger.error(
+ `Error in clearing expiration and status for retry! Report: ` + jobDebugMessage(report)
+ );
this.logger.error(err);
throw err;
}
}
/*
- * A zombie report document is one that isn't completed or failed, isn't
- * being executed, and isn't scheduled to run. They arise:
- * - when the cluster has processing documents in ESQueue before upgrading to v7.13 when ESQueue was removed
- * - if Kibana crashes while a report task is executing and it couldn't be rescheduled on its own
- *
- * Pending reports are not included in this search: they may be scheduled in TM just not run yet.
- * TODO Should we get a list of the reports that are pending and scheduled in TM so we can exclude them from this query?
+ * A report needs to be rescheduled when:
+ * 1. An older version of Kibana created jobs with ESQueue, and they have
+ * not yet started running.
+ * 2. The report process_expiration field is overdue, which happens if the
+ * report runs too long or Kibana restarts during execution
*/
- public async findZombieReportDocuments(): Promise {
+ public async findStaleReportJob(): Promise {
const client = await this.getClient();
+
+ const expiredFilter = {
+ bool: {
+ must: [
+ { range: { process_expiration: { lt: `now` } } },
+ { terms: { status: [statuses.JOB_STATUS_PROCESSING] } },
+ ],
+ },
+ };
+ const oldVersionFilter = {
+ bool: {
+ must: [{ terms: { status: [statuses.JOB_STATUS_PENDING] } }],
+ must_not: [{ exists: { field: 'migration_version' } }],
+ },
+ };
+
const { body } = await client.search({
+ size: 1,
index: this.indexPrefix + '-*',
- filter_path: 'hits.hits',
+ seq_no_primary_term: true,
+ _source_excludes: ['output'],
body: {
- sort: { created_at: { order: 'desc' } },
- query: {
- bool: {
- filter: [
- {
- bool: {
- must: [
- { range: { process_expiration: { lt: `now-${this.queueTimeoutMins}m` } } },
- { terms: { status: [statuses.JOB_STATUS_PROCESSING] } },
- ],
- },
- },
- ],
- },
- },
+ sort: { created_at: { order: 'asc' as const } }, // find the oldest first
+ query: { bool: { filter: { bool: { should: [expiredFilter, oldVersionFilter] } } } },
},
});
- return body.hits?.hits as ReportRecordTimeout[];
+ return body.hits?.hits[0] as ReportRecordTimeout;
}
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
index 2960ce457b7ae..f9e2cd82b0805 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { UpdateResponse } from '@elastic/elasticsearch/api/types';
import moment from 'moment';
import * as Rx from 'rxjs';
import { timeout } from 'rxjs/operators';
@@ -19,9 +20,9 @@ import { CancellationToken } from '../../../common';
import { durationToNumber, numberToDuration } from '../../../common/schema_utils';
import { ReportingConfigType } from '../../config';
import { BasePayload, RunTaskFn } from '../../types';
-import { Report, ReportingStore } from '../store';
+import { Report, ReportDocument, ReportingStore } from '../store';
+import { ReportFailedFields, ReportProcessingFields } from '../store/store';
import {
- ReportingExecuteTaskInstance,
ReportingTask,
ReportingTaskStatus,
REPORTING_EXECUTE_TYPE,
@@ -30,6 +31,13 @@ import {
} from './';
import { errorLogger } from './error_logger';
+interface ReportingExecuteTaskInstance {
+ state: object;
+ taskType: string;
+ params: ReportTaskParams;
+ runAt?: Date;
+}
+
function isOutput(output: TaskRunResult | Error): output is TaskRunResult {
return typeof output === 'object' && (output as TaskRunResult).content != null;
}
@@ -101,15 +109,21 @@ export class ExecuteReportTask implements ReportingTask {
}
public async _claimJob(task: ReportTaskParams): Promise {
- const store = await this.getStore();
+ if (this.kibanaId == null) {
+ throw new Error(`Kibana instance ID is undefined!`);
+ }
+ if (this.kibanaName == null) {
+ throw new Error(`Kibana instance name is undefined!`);
+ }
+ const store = await this.getStore();
let report: Report;
if (task.id && task.index) {
// if this is an ad-hoc report, there is a corresponding "pending" record in ReportingStore in need of updating
- report = await store.findReportFromTask(task); // update seq_no
+ report = await store.findReportFromTask(task); // receives seq_no and primary_term
} else {
// if this is a scheduled report (not implemented), the report object needs to be instantiated
- throw new Error('scheduled reports are not supported!');
+ throw new Error('Could not find matching report document!');
}
// Check if this is a completed job. This may happen if the `reports:monitor`
@@ -126,7 +140,7 @@ export class ExecuteReportTask implements ReportingTask {
const maxAttempts = task.max_attempts;
if (report.attempts >= maxAttempts) {
const err = new Error(`Max attempts reached (${maxAttempts}). Queue timeout reached.`);
- await this._failJob(task, err);
+ await this._failJob(report, err);
throw err;
}
@@ -134,7 +148,7 @@ export class ExecuteReportTask implements ReportingTask {
const startTime = m.toISOString();
const expirationTime = m.add(queueTimeout).toISOString();
- const stats = {
+ const doc: ReportProcessingFields = {
kibana_id: this.kibanaId,
kibana_name: this.kibanaName,
browser_type: this.config.capture.browser.type,
@@ -144,19 +158,28 @@ export class ExecuteReportTask implements ReportingTask {
process_expiration: expirationTime,
};
- this.logger.debug(`Claiming ${report.jobtype} job ${report._id}`);
-
const claimedReport = new Report({
...report,
- ...stats,
+ ...doc,
});
- await store.setReportClaimed(claimedReport, stats);
+ this.logger.debug(
+ `Claiming ${claimedReport.jobtype} ${report._id} ` +
+ `[_index: ${report._index}] ` +
+ `[_seq_no: ${report._seq_no}] ` +
+ `[_primary_term: ${report._primary_term}] ` +
+ `[attempts: ${report.attempts}] ` +
+ `[process_expiration: ${expirationTime}]`
+ );
+
+ const resp = await store.setReportClaimed(claimedReport, doc);
+ claimedReport._seq_no = resp._seq_no;
+ claimedReport._primary_term = resp._primary_term;
return claimedReport;
}
- private async _failJob(task: ReportTaskParams, error?: Error) {
- const message = `Failing ${task.jobtype} job ${task.id}`;
+ private async _failJob(report: Report, error?: Error): Promise> {
+ const message = `Failing ${report.jobtype} job ${report._id}`;
// log the error
let docOutput;
@@ -169,9 +192,8 @@ export class ExecuteReportTask implements ReportingTask {
// update the report in the store
const store = await this.getStore();
- const report = await store.findReportFromTask(task);
const completedTime = moment().toISOString();
- const doc = {
+ const doc: ReportFailedFields = {
completed_at: completedTime,
output: docOutput,
};
@@ -179,7 +201,7 @@ export class ExecuteReportTask implements ReportingTask {
return await store.setReportFailed(report, doc);
}
- private _formatOutput(output: TaskRunResult | Error) {
+ private _formatOutput(output: TaskRunResult | Error): TaskRunResult {
const docOutput = {} as TaskRunResult;
const unknownMime = null;
@@ -201,7 +223,10 @@ export class ExecuteReportTask implements ReportingTask {
return docOutput;
}
- public async _performJob(task: ReportTaskParams, cancellationToken: CancellationToken) {
+ public async _performJob(
+ task: ReportTaskParams,
+ cancellationToken: CancellationToken
+ ): Promise {
if (!this.taskExecutors) {
throw new Error(`Task run function factories have not been called yet!`);
}
@@ -220,10 +245,10 @@ export class ExecuteReportTask implements ReportingTask {
.toPromise();
}
- public async _completeJob(task: ReportTaskParams, output: TaskRunResult) {
- let docId = `/${task.index}/_doc/${task.id}`;
+ public async _completeJob(report: Report, output: TaskRunResult): Promise {
+ let docId = `/${report._index}/_doc/${report._id}`;
- this.logger.info(`Saving ${task.jobtype} job ${docId}.`);
+ this.logger.debug(`Saving ${report.jobtype} to ${docId}.`);
const completedTime = moment().toISOString();
const docOutput = this._formatOutput(output);
@@ -233,16 +258,13 @@ export class ExecuteReportTask implements ReportingTask {
completed_at: completedTime,
output: docOutput,
};
- const report = await store.findReportFromTask(task); // update seq_no and primary_term
docId = `/${report._index}/_doc/${report._id}`;
- try {
- await store.setReportCompleted(report, doc);
- this.logger.debug(`Saved ${report.jobtype} job ${docId}`);
- } catch (err) {
- if (err.statusCode === 409) return false;
- errorLogger(this.logger, `Failure saving completed job ${docId}!`);
- }
+ const resp = await store.setReportCompleted(report, doc);
+ this.logger.info(`Saved ${report.jobtype} job ${docId}`);
+ report._seq_no = resp._seq_no;
+ report._primary_term = resp._primary_term;
+ return report;
}
/*
@@ -264,7 +286,6 @@ export class ExecuteReportTask implements ReportingTask {
*/
run: async () => {
let report: Report | undefined;
- let attempts = 0;
// find the job in the store and set status to processing
const task = context.taskInstance.params as ReportTaskParams;
@@ -278,64 +299,73 @@ export class ExecuteReportTask implements ReportingTask {
// Update job status to claimed
report = await this._claimJob(task);
-
- const { jobtype: jobType, attempts: attempt, max_attempts: maxAttempts } = task;
- this.logger.info(
- `Starting ${jobType} report ${jobId}: attempt ${attempt + 1} of ${maxAttempts}.`
- );
- this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
} catch (failedToClaim) {
// error claiming report - log the error
// could be version conflict, or no longer connected to ES
- errorLogger(this.logger, `Error in claiming report!`, failedToClaim);
+ errorLogger(this.logger, `Error in claiming ${jobId}`, failedToClaim);
}
if (!report) {
- errorLogger(this.logger, `Report could not be claimed. Exiting...`);
+ errorLogger(this.logger, `Job ${jobId} could not be claimed. Exiting...`);
return;
}
- attempts = report.attempts;
+ const { jobtype: jobType, attempts, max_attempts: maxAttempts } = report;
+ this.logger.debug(
+ `Starting ${jobType} report ${jobId}: attempt ${attempts} of ${maxAttempts}.`
+ );
+ this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
try {
const output = await this._performJob(task, cancellationToken);
if (output) {
- await this._completeJob(task, output);
+ report = await this._completeJob(report, output);
}
-
// untrack the report for concurrency awareness
this.logger.debug(`Stopping ${jobId}.`);
- this.reporting.untrackReport(jobId);
- this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
} catch (failedToExecuteErr) {
cancellationToken.cancel();
- const maxAttempts = this.config.capture.maxAttempts;
if (attempts < maxAttempts) {
- // attempts remain - reschedule
+ // attempts remain, reschedule
try {
+ if (report == null) {
+ throw new Error(`Report ${jobId} is null!`);
+ }
// reschedule to retry
const remainingAttempts = maxAttempts - report.attempts;
errorLogger(
this.logger,
- `Scheduling retry. Retries remaining: ${remainingAttempts}.`,
+ `Scheduling retry for job ${jobId}. Retries remaining: ${remainingAttempts}.`,
failedToExecuteErr
);
await this.rescheduleTask(reportFromTask(task).toReportTaskJSON(), this.logger);
} catch (rescheduleErr) {
// can not be rescheduled - log the error
- errorLogger(this.logger, `Could not reschedule the errored job!`, rescheduleErr);
+ errorLogger(
+ this.logger,
+ `Could not reschedule the errored job ${jobId}!`,
+ rescheduleErr
+ );
}
} else {
// 0 attempts remain - fail the job
try {
- const maxAttemptsMsg = `Max attempts reached (${attempts}). Failed with: ${failedToExecuteErr}`;
- await this._failJob(task, new Error(maxAttemptsMsg));
+ const maxAttemptsMsg = `Max attempts (${attempts}) reached for job ${jobId}. Failed with: ${failedToExecuteErr}`;
+ if (report == null) {
+ throw new Error(`Report ${jobId} is null!`);
+ }
+ const resp = await this._failJob(report, new Error(maxAttemptsMsg));
+ report._seq_no = resp._seq_no;
+ report._primary_term = resp._primary_term;
} catch (failedToFailError) {
- errorLogger(this.logger, `Could not fail the job!`, failedToFailError);
+ errorLogger(this.logger, `Could not fail ${jobId}!`, failedToFailError);
}
}
+ } finally {
+ this.reporting.untrackReport(jobId);
+ this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
}
},
@@ -374,11 +404,12 @@ export class ExecuteReportTask implements ReportingTask {
state: {},
params: report,
};
+
return await this.getTaskManagerStart().schedule(taskInstance);
}
private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) {
- logger.info(`Rescheduling ${task.id} to retry after error.`);
+ logger.info(`Rescheduling task:${task.id} to retry after error.`);
const oldTaskInstance: ReportingExecuteTaskInstance = {
taskType: REPORTING_EXECUTE_TYPE,
@@ -386,7 +417,7 @@ export class ExecuteReportTask implements ReportingTask {
params: task,
};
const newTask = await this.getTaskManagerStart().schedule(oldTaskInstance);
- logger.debug(`Rescheduled ${task.id}`);
+ logger.debug(`Rescheduled task:${task.id}. New task: task:${newTask.id}`);
return newTask;
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts
index ec9e85e957d03..c02b06d97adc7 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/index.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts
@@ -32,13 +32,6 @@ export interface ReportTaskParams {
meta: ReportSource['meta'];
}
-export interface ReportingExecuteTaskInstance /* extends TaskInstanceWithDeprecatedFields */ {
- state: object;
- taskType: string;
- params: ReportTaskParams;
- runAt?: Date;
-}
-
export enum ReportingTaskStatus {
UNINITIALIZED = 'uninitialized',
INITIALIZED = 'initialized',
@@ -52,6 +45,5 @@ export interface ReportingTask {
maxAttempts: number;
timeout: string;
};
-
getStatus: () => ReportingTaskStatus;
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
index 36380f767e6d9..9e1bc49739c93 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
@@ -11,21 +11,29 @@ import { ReportingCore } from '../../';
import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../../task_manager/server';
import { numberToDuration } from '../../../common/schema_utils';
import { ReportingConfigType } from '../../config';
+import { statuses } from '../statuses';
import { Report } from '../store';
-import {
- ReportingExecuteTaskInstance,
- ReportingTask,
- ReportingTaskStatus,
- REPORTING_EXECUTE_TYPE,
- REPORTING_MONITOR_TYPE,
- ReportTaskParams,
-} from './';
+import { ReportingTask, ReportingTaskStatus, REPORTING_MONITOR_TYPE, ReportTaskParams } from './';
/*
- * Task for finding the ReportingRecords left in the ReportingStore and stuck
- * in pending or processing. It could happen if the server crashed while running
- * a report and was cancelled. Normally a failure would mean scheduling a
- * retry or failing the report, but the retry is not guaranteed to be scheduled.
+ * Task for finding the ReportingRecords left in the ReportingStore (.reporting index) and stuck in
+ * a pending or processing status.
+ *
+ * Stuck in pending:
+ * - This can happen if the report was scheduled in an earlier version of Kibana that used ESQueue.
+ * - Task Manager doesn't know about these types of reports because there was never a task
+ * scheduled for them.
+ * Stuck in processing:
+ * - This can could happen if the server crashed while a report was executing.
+ * - Task Manager doesn't know about these reports, because the task is completed in Task
+ * Manager when Reporting starts executing the report. We are not using Task Manager's retry
+ * mechanisms, which defer the retry for a few minutes.
+ *
+ * These events require us to reschedule the report with Task Manager, so that the jobs can be
+ * distributed and executed.
+ *
+ * The runner function reschedules a single report job per task run, to avoid flooding Task Manager
+ * in case many report jobs need to be recovered.
*/
export class MonitorReportsTask implements ReportingTask {
public TYPE = REPORTING_MONITOR_TYPE;
@@ -77,36 +85,41 @@ export class MonitorReportsTask implements ReportingTask {
const reportingStore = await this.getStore();
try {
- const results = await reportingStore.findZombieReportDocuments();
- if (results && results.length) {
- this.logger.info(
- `Found ${results.length} reports to reschedule: ${results
- .map((pending) => pending._id)
- .join(',')}`
- );
- } else {
- this.logger.debug(`Found 0 pending reports.`);
+ const recoveredJob = await reportingStore.findStaleReportJob();
+ if (!recoveredJob) {
+ // no reports need to be rescheduled
return;
}
- for (const pending of results) {
- const {
- _id: jobId,
- _source: { process_expiration: processExpiration, status },
- } = pending;
- const expirationTime = moment(processExpiration); // If it is the start of the Epoch, something went wrong
- const timeWaitValue = moment().valueOf() - expirationTime.valueOf();
- const timeWaitTime = moment.duration(timeWaitValue);
+ const {
+ _id: jobId,
+ _source: { process_expiration: processExpiration, status },
+ } = recoveredJob;
+
+ if (![statuses.JOB_STATUS_PENDING, statuses.JOB_STATUS_PROCESSING].includes(status)) {
+ throw new Error(`Invalid job status in the monitoring search result: ${status}`); // only pending or processing jobs possibility need rescheduling
+ }
+
+ if (status === statuses.JOB_STATUS_PENDING) {
this.logger.info(
- `Task ${jobId} has ${status} status for ${timeWaitTime.humanize()}. The queue timeout is ${this.timeout.humanize()}.`
+ `${jobId} was scheduled in a previous version and left in [${status}] status. Rescheduling...`
);
+ }
- // clear process expiration and reschedule
- const oldReport = new Report({ ...pending, ...pending._source });
- const reschedulingTask = oldReport.toReportTaskJSON();
- await reportingStore.clearExpiration(oldReport);
- await this.rescheduleTask(reschedulingTask, this.logger);
+ if (status === statuses.JOB_STATUS_PROCESSING) {
+ const expirationTime = moment(processExpiration);
+ const overdueValue = moment().valueOf() - expirationTime.valueOf();
+ this.logger.info(
+ `${jobId} status is [${status}] and the expiration time was [${overdueValue}ms] ago. Rescheduling...`
+ );
}
+
+ // clear process expiration and set status to pending
+ const report = new Report({ ...recoveredJob, ...recoveredJob._source });
+ await reportingStore.prepareReportForRetry(report); // if there is a version conflict response, this just throws and logs an error
+
+ // clear process expiration and reschedule
+ await this.rescheduleTask(report.toReportTaskJSON(), this.logger); // a recovered report job must be scheduled by only a sinle Kibana instance
} catch (err) {
this.logger.error(err);
}
@@ -126,33 +139,19 @@ export class MonitorReportsTask implements ReportingTask {
createTaskRunner: this.getTaskRunner(),
maxAttempts: 1,
// round the timeout value up to the nearest second, since Task Manager
- // doesn't support milliseconds
+ // doesn't support milliseconds or > 1s
timeout: Math.ceil(this.timeout.asSeconds()) + 's',
};
}
- // reschedule the task with TM and update the report document status to "Pending"
+ // reschedule the task with TM
private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) {
if (!this.taskManagerStart) {
throw new Error('Reporting task runner has not been initialized!');
}
- logger.info(`Rescheduling ${task.id} to retry after timeout expiration.`);
-
- const store = await this.getStore();
-
- const oldTaskInstance: ReportingExecuteTaskInstance = {
- taskType: REPORTING_EXECUTE_TYPE, // schedule a task to EXECUTE
- state: {},
- params: task,
- };
-
- const [report, newTask] = await Promise.all([
- await store.findReportFromTask(task),
- await this.taskManagerStart.schedule(oldTaskInstance),
- ]);
-
- await store.setReportPending(report);
+ logger.info(`Rescheduling task:${task.id} to retry.`);
+ const newTask = await this.reporting.scheduleTask(task);
return newTask;
}
diff --git a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
index 9e3bd491115ce..ddf69167145f1 100644
--- a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
+++ b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
@@ -4,11 +4,3 @@
.rollupJobWizardStepActions {
align-items: flex-end; /* 1 */
}
-
-/**
- * 1. Ensure panel fills width of parent when search input yields no matching rollup jobs.
- */
-.rollupJobsListPanel {
- // sass-lint:disable-block no-important
- flex-grow: 1 !important; /* 1 */
-}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
index fa3ce260424f2..6f22345dc1cec 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { cloneDeep, debounce, first, mapValues } from 'lodash';
@@ -18,11 +18,10 @@ import {
EuiCallOut,
EuiLoadingKibana,
EuiOverlayMask,
- EuiPageContent,
- EuiPageContentHeader,
+ EuiPageContentBody,
+ EuiPageHeader,
EuiSpacer,
EuiStepsHorizontal,
- EuiTitle,
} from '@elastic/eui';
import {
@@ -522,44 +521,46 @@ export class JobCreateUi extends Component {
}
saveErrorFeedback = (
-
+ <>
+
+
{errorBody}
-
+ >
);
}
return (
-
-
-
-
-
-
-
-
-
-
- {saveErrorFeedback}
-
-
+
+
+ }
+ />
-
+
+
+
+
+ {saveErrorFeedback}
+
+
+
+ {this.renderCurrentStep()}
- {this.renderCurrentStep()}
+
-
+ {this.renderNavigation()}
- {this.renderNavigation()}
-
{savingFeedback}
-
+
);
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
index 4fe1674e8c643..5e97ff5e2980d 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
@@ -195,7 +195,7 @@ export class DetailPanel extends Component {
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
index 16919b8388e2e..e1f9ec2b3a315 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
@@ -70,7 +70,7 @@ describe(' ', () => {
({ component, find, exists } = initTestBed({ isLoading: true }));
const loading = find('rollupJobDetailLoading');
expect(loading.length).toBeTruthy();
- expect(loading.text()).toEqual('Loading rollup job...');
+ expect(loading.text()).toEqual('Loading rollup job…');
// Make sure the title and the tabs are visible
expect(exists('detailPanelTabSelected')).toBeTruthy();
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
index 589546a11ef38..b2448eb610774 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
@@ -12,24 +12,19 @@ import { i18n } from '@kbn/i18n';
import {
EuiButton,
+ EuiButtonEmpty,
EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
+ EuiPageHeader,
EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
- EuiText,
- EuiTextColor,
- EuiTitle,
- EuiCallOut,
} from '@elastic/eui';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouterLinkProps, listBreadcrumb } from '../../services';
+import { documentationLinks } from '../../services/documentation_links';
+
import { JobTable } from './job_table';
import { DetailPanel } from './detail_panel';
@@ -87,38 +82,26 @@ export class JobListUi extends Component {
this.props.closeDetailPanel();
}
- getHeaderSection() {
- return (
-
-
-
-
-
-
-
- );
- }
-
renderNoPermission() {
const title = i18n.translate('xpack.rollupJobs.jobList.noPermissionTitle', {
defaultMessage: 'Permission error',
});
return (
-
- {this.getHeaderSection()}
-
-
+
-
-
-
+ iconType="alert"
+ title={{title} }
+ body={
+
+
+
+ }
+ />
+
);
}
@@ -130,101 +113,110 @@ export class JobListUi extends Component {
const title = i18n.translate('xpack.rollupJobs.jobList.loadingErrorTitle', {
defaultMessage: 'Error loading rollup jobs',
});
+
return (
-
- {this.getHeaderSection()}
-
-
- {statusCode} {errorString}
-
-
+
+ {title}}
+ body={
+
+ {statusCode} {errorString}
+
+ }
+ />
+
);
}
renderEmpty() {
return (
-
-
-
- }
- body={
-
-
+
+
+
+ }
+ body={
+
+
+
+
+
+ }
+ actions={
+
+
-
-
- }
- actions={
-
-
-
- }
- />
+
+ }
+ />
+
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
renderList() {
- const { isLoading } = this.props;
-
return (
-
-
- {this.getHeaderSection()}
-
-
-
+ <>
+
+
+
+ }
+ rightSideItems={[
+
-
-
-
+ ,
+ ]}
+ />
- {isLoading ? this.renderLoading() : }
+
+
+
-
+ >
);
}
@@ -241,15 +233,13 @@ export class JobListUi extends Component {
}
} else if (!isLoading && !hasJobs) {
content = this.renderEmpty();
+ } else if (isLoading) {
+ content = this.renderLoading();
} else {
content = this.renderList();
}
- return (
-
- {content}
-
- );
+ return content;
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
index 3283f4f521fc0..b2c738a033b3c 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
@@ -22,6 +22,15 @@ jest.mock('../../services', () => {
};
});
+jest.mock('../../services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const defaultProps = {
history: { location: {} },
loadJobs: () => {},
@@ -52,14 +61,14 @@ describe(' ', () => {
it('should display a loading message when loading the jobs', () => {
const { component, exists } = initTestBed({ isLoading: true });
- expect(exists('jobListLoading')).toBeTruthy();
+ expect(exists('sectionLoading')).toBeTruthy();
expect(component.find('JobTable').length).toBeFalsy();
});
it('should display the when there are jobs', () => {
const { component, exists } = initTestBed({ hasJobs: true });
- expect(exists('jobListLoading')).toBeFalsy();
+ expect(exists('sectionLoading')).toBeFalsy();
expect(component.find('JobTable').length).toBeTruthy();
});
@@ -71,21 +80,20 @@ describe(' ', () => {
},
});
- it('should display a callout with the status and the message', () => {
+ it('should display an error with the status and the message', () => {
expect(exists('jobListError')).toBeTruthy();
expect(find('jobListError').find('EuiText').text()).toEqual('400 Houston we got a problem.');
});
});
describe('when the user does not have the permission to access it', () => {
- const { exists } = initTestBed({ jobLoadError: { status: 403 } });
+ const { exists, find } = initTestBed({ jobLoadError: { status: 403 } });
- it('should render a callout message', () => {
+ it('should render an error message', () => {
expect(exists('jobListNoPermission')).toBeTruthy();
- });
-
- it('should display the page header', () => {
- expect(exists('jobListPageHeader')).toBeTruthy();
+ expect(find('jobListNoPermission').find('EuiText').text()).toEqual(
+ 'You do not have permission to view or add rollup jobs.'
+ );
});
});
});
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
index fe3d2cbd4cbe0..83135cf219f35 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -28,10 +28,11 @@ import {
EuiTableRowCellCheckbox,
EuiText,
EuiToolTip,
+ EuiButton,
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common';
-import { METRIC_TYPE } from '../../../services';
+import { METRIC_TYPE, getRouterLinkProps } from '../../../services';
import { trackUiMetric } from '../../../../kibana_services';
import { JobActionMenu, JobStatus } from '../../components';
@@ -346,9 +347,9 @@ export class JobTable extends Component {
const atLeastOneItemSelected = Object.keys(idToSelectedJobMap).length > 0;
return (
-
-
- {atLeastOneItemSelected ? (
+
+
+ {atLeastOneItemSelected && (
- ) : null}
+ )}
+
+
+
+
+
@@ -409,7 +418,7 @@ export class JobTable extends Component {
{jobs.length > 0 ? this.renderPager() : null}
-
+
);
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
index 3fa879923c40a..d52f3fa35a544 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
@@ -20,6 +20,14 @@ jest.mock('../../../../kibana_services', () => {
};
});
+jest.mock('../../../services', () => {
+ const services = jest.requireActual('../../../services');
+ return {
+ ...services,
+ getRouterLinkProps: (link) => ({ href: link }),
+ };
+});
+
const defaultProps = {
jobs: [],
pager: new Pager(20, 10, 1),
diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
index 0dc3a02d3c077..c63d01f3c200d 100644
--- a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
+++ b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
@@ -5,9 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-
-import { loadJobs as sendLoadJobsRequest, deserializeJobs, showApiError } from '../../services';
+import { loadJobs as sendLoadJobsRequest, deserializeJobs } from '../../services';
import { LOAD_JOBS_START, LOAD_JOBS_SUCCESS, LOAD_JOBS_FAILURE } from '../action_types';
export const loadJobs = () => async (dispatch) => {
@@ -19,17 +17,10 @@ export const loadJobs = () => async (dispatch) => {
try {
jobs = await sendLoadJobsRequest();
} catch (error) {
- dispatch({
+ return dispatch({
type: LOAD_JOBS_FAILURE,
payload: { error },
});
-
- return showApiError(
- error,
- i18n.translate('xpack.rollupJobs.loadAction.errorTitle', {
- defaultMessage: 'Error loading rollup jobs',
- })
- );
}
dispatch({
diff --git a/x-pack/plugins/rollup/public/shared_imports.ts b/x-pack/plugins/rollup/public/shared_imports.ts
index fd28175318666..c8d7f1d9f13f3 100644
--- a/x-pack/plugins/rollup/public/shared_imports.ts
+++ b/x-pack/plugins/rollup/public/shared_imports.ts
@@ -5,4 +5,8 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+} from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
index fa1a786bc8a71..46ddfbcfc2de5 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { getRouter, setHttp } from '../../crud_app/services';
+import { getRouter, setHttp, init as initDocumentation } from '../../crud_app/services';
import { mockHttpRequest, pageHelpers, nextTick } from './helpers';
import { JOBS } from './helpers/constants';
-import { coreMock } from '../../../../../../src/core/public/mocks';
+import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks';
jest.mock('../../crud_app/services', () => {
const services = jest.requireActual('../../crud_app/services');
@@ -38,6 +38,7 @@ describe(' ', () => {
beforeAll(() => {
startMock = coreMock.createStart();
setHttp(startMock.http);
+ initDocumentation(docLinksServiceMock.createStartContract());
});
beforeEach(async () => {
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
index cfb63893ee423..3987e18538e57 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
@@ -24,6 +24,15 @@ jest.mock('../../kibana_services', () => {
};
});
+jest.mock('../../crud_app/services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const { setup } = pageHelpers.jobList;
describe('Smoke test cloning an existing rollup job from job list', () => {
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx
index 749c1c8ccb4e2..48a0d18653053 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx
@@ -12,7 +12,6 @@ import {
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
- EuiOverlayMask,
EuiTitle,
} from '@elastic/eui';
import React, { Fragment, useState } from 'react';
@@ -47,37 +46,39 @@ export const PrivilegeSummary = (props: Props) => {
/>
{isOpen && (
-
- setIsOpen(false)} size={flyoutSize}>
-
-
-
-
-
-
-
-
-
-
-
- setIsOpen(false)}>
+ setIsOpen(false)}
+ size={flyoutSize}
+ maskProps={{ headerZindexLocation: 'below' }}
+ >
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ setIsOpen(false)}>
+
+
+
+
)}
);
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
index b290cb301866d..8f62acd463e6a 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
@@ -20,7 +20,6 @@ import {
EuiFlyoutHeader,
EuiForm,
EuiFormRow,
- EuiOverlayMask,
EuiSpacer,
EuiText,
EuiTitle,
@@ -93,64 +92,67 @@ export class PrivilegeSpaceForm extends Component {
public render() {
return (
-
-
-
-
-
-
-
-
-
-
- {this.getForm()}
-
-
- {this.state.privilegeCalculator.hasSupersededInheritedPrivileges(
- this.state.privilegeIndex
- ) && (
-
-
- }
- >
-
-
-
-
- )}
-
-
-
+
+
+
+
+
+
+
+
+
+ {this.getForm()}
+
+
+ {this.state.privilegeCalculator.hasSupersededInheritedPrivileges(
+ this.state.privilegeIndex
+ ) && (
+
+
-
-
- {this.getSaveButton()}
-
-
-
-
+ }
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+ {this.getSaveButton()}
+
+
+
);
}
diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
index eb266ce93338c..f36a1bf477b06 100644
--- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
+++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap
@@ -58,33 +58,33 @@ exports[` renders permission denied if required 1`] = `
/>
+
+
+
+ You need permission to manage roles
+
+
+
-
-
-
- You need permission to manage roles
-
-
-
diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
index bcb97538b4f05..2ee2337fc9aeb 100644
--- a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
+++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`PromptPage renders as expected with additional scripts 1`] = `"Elastic MockedFonts "`;
+exports[`PromptPage renders as expected with additional scripts 1`] = `"Elastic MockedFonts "`;
-exports[`PromptPage renders as expected without additional scripts 1`] = `"Elastic MockedFonts "`;
+exports[`PromptPage renders as expected without additional scripts 1`] = `"Elastic MockedFonts "`;
diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
index 55168401992f7..2e7f3d49e478f 100644
--- a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
+++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`UnauthenticatedPage renders as expected 1`] = `"Elastic MockedFonts
We couldn't log you in
We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.
"`;
+exports[`UnauthenticatedPage renders as expected 1`] = `"Elastic MockedFonts
We couldn't log you in
We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.
"`;
diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
index 1011d82eb1f73..8d31770cd9385 100644
--- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
+++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ResetSessionPage renders as expected 1`] = `"Elastic MockedFonts
You do not have permission to access the requested page
Either go back to the previous page or log in as a different user.
"`;
+exports[`ResetSessionPage renders as expected 1`] = `"Elastic MockedFonts
You do not have permission to access the requested page
Either go back to the previous page or log in as a different user.
"`;
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index e65ff1afcc9c3..d59d7e7b7da4f 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -44,7 +44,8 @@ export const DEFAULT_INTERVAL_VALUE = 300000; // ms
export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges';
export const DEFAULT_TRANSFORMS = 'securitySolution:transforms';
export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled';
-export const GLOBAL_HEADER_HEIGHT = 98; // px
+export const GLOBAL_HEADER_HEIGHT = 96; // px
+export const GLOBAL_HEADER_HEIGHT_WITH_GLOBAL_BANNER = 128; // px
export const FILTERS_GLOBAL_HEIGHT = 109; // px
export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled';
export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51';
@@ -240,6 +241,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [
'.email',
'.slack',
'.pagerduty',
+ '.swimlane',
'.webhook',
'.servicenow',
'.jira',
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
index 99753242e7627..dfaad68e295eb 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts
@@ -58,7 +58,6 @@ export interface ActivityLogActionResponse {
}
export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse;
export interface ActivityLog {
- total: number;
page: number;
pageSize: number;
data: ActivityLogEntry[];
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index b20b1501eecc5..a9a81aa285af7 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -15,6 +15,7 @@ const allowedExperimentalValues = Object.freeze({
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
+ tGridEnabled: false,
});
type ExperimentalConfigKeys = Array;
diff --git a/x-pack/plugins/security_solution/common/index.ts b/x-pack/plugins/security_solution/common/index.ts
index 1fec1c76430eb..e6d7bcc9bd506 100644
--- a/x-pack/plugins/security_solution/common/index.ts
+++ b/x-pack/plugins/security_solution/common/index.ts
@@ -4,3 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
+export * from './types';
+export * from './search_strategy';
+export * from './utility_types';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
index 4fcfbdac3c1b4..095ba4ca20afc 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
@@ -4,52 +4,27 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import type { estypes } from '@elastic/elasticsearch';
import { IEsSearchResponse } from '../../../../../../src/plugins/data/common';
+export type {
+ Inspect,
+ SortField,
+ TimerangeInput,
+ PaginationInputPaginated,
+ DocValueFields,
+ CursorType,
+ TotalValue,
+} from '../../../../timelines/common';
+export { Direction } from '../../../../timelines/common';
export type Maybe = T | null;
export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0];
-export interface TotalValue {
- value: number;
- relation: string;
-}
-
-export interface Inspect {
- dsl: string[];
-}
-
export interface PageInfoPaginated {
activePage: number;
fakeTotalCount: number;
showMorePagesIndicator: boolean;
}
-
-export interface CursorType {
- value?: Maybe;
- tiebreaker?: Maybe;
-}
-
-export enum Direction {
- asc = 'asc',
- desc = 'desc',
-}
-
-export interface SortField {
- field: Field;
- direction: Direction;
-}
-
-export interface TimerangeInput {
- /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
- interval: string;
- /** The end of the timerange */
- to: string;
- /** The beginning of the timerange */
- from: string;
-}
-
export interface PaginationInput {
/** The limit parameter allows you to configure the maximum amount of items to be returned */
limit: number;
@@ -59,19 +34,6 @@ export interface PaginationInput {
tiebreaker?: Maybe;
}
-export interface PaginationInputPaginated {
- /** The activePage parameter defines the page of results you want to fetch */
- activePage: number;
- /** The cursorStart parameter defines the start of the results to be displayed */
- cursorStart: number;
- /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
- fakePossibleCount: number;
- /** The querySize parameter is the number of items to be returned */
- querySize: number;
-}
-
-export type DocValueFields = estypes.SearchDocValueField;
-
export interface Explanation {
value: number;
description: string;
@@ -111,13 +73,3 @@ export interface GenericBuckets {
}
export type StringOrNumber = string | number;
-
-export interface TimerangeFilter {
- range: {
- [timestamp: string]: {
- gte: string;
- lte: string;
- format: string;
- };
- };
-}
diff --git a/x-pack/plugins/security_solution/common/search_strategy/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index.ts
index 575256b991d16..e3d6736878063 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/index.ts
@@ -8,3 +8,4 @@
export * from './common';
export * from './security_solution';
export * from './timeline';
+export * from './index_fields';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
index d747758640fab..4e5f8af41a2ef 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
@@ -5,37 +5,10 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Ecs } from '../../../../ecs';
-import { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
-import { TimelineRequestOptionsPaginated } from '../..';
-
-export interface TimelineEdges {
- node: TimelineItem;
- cursor: CursorType;
-}
-
-export interface TimelineItem {
- _id: string;
- _index?: Maybe;
- data: TimelineNonEcsData[];
- ecs: Ecs;
-}
-
-export interface TimelineNonEcsData {
- field: string;
- value?: Maybe;
-}
-
-export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse {
- edges: TimelineEdges[];
- totalCount: number;
- pageInfo: Pick;
- inspect?: Maybe;
-}
-
-export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated {
- fields: string[] | Array<{ field: string; include_unmapped: boolean }>;
- fieldRequested: string[];
- language: 'eql' | 'kuery' | 'lucene';
-}
+export type {
+ TimelineEdges,
+ TimelineItem,
+ TimelineNonEcsData,
+ TimelineEventsAllStrategyResponse,
+ TimelineEventsAllRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
index 4a5bd2c99a0eb..e4d2ea52ffdff 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
@@ -5,22 +5,8 @@
* 2.0.
*/
-import { Ecs } from '../../../../ecs';
-import { CursorType, Maybe } from '../../../common';
-
-export interface TimelineEdges {
- node: TimelineItem;
- cursor: CursorType;
-}
-
-export interface TimelineItem {
- _id: string;
- _index?: Maybe;
- data: TimelineNonEcsData[];
- ecs: Ecs;
-}
-
-export interface TimelineNonEcsData {
- field: string;
- value?: Maybe;
-}
+export type {
+ TimelineEdges,
+ TimelineItem,
+ TimelineNonEcsData,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
index 1f9820f8e5c2b..3fd13e56cc7e7 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
@@ -5,27 +5,8 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe } from '../../../common';
-import { TimelineRequestOptionsPaginated } from '../..';
-
-export interface TimelineEventsDetailsItem {
- ariaRowindex?: Maybe;
- category?: string;
- field: string;
- values?: Maybe;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- originalValue?: Maybe;
- isObjectArray: boolean;
-}
-
-export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse {
- data?: Maybe;
- inspect?: Maybe;
-}
-
-export interface TimelineEventsDetailsRequestOptions
- extends Partial {
- indexName: string;
- eventId: string;
-}
+export type {
+ TimelineEventsDetailsItem,
+ TimelineEventsDetailsStrategyResponse,
+ TimelineEventsDetailsRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
index c508876032fca..10e9bbd7670cd 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
@@ -5,43 +5,10 @@
* 2.0.
*/
-import { EuiComboBoxOptionOption } from '@elastic/eui';
-import {
- EqlSearchStrategyRequest,
- EqlSearchStrategyResponse,
-} from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe, PaginationInputPaginated } from '../../..';
-import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..';
-import { EqlSearchResponse } from '../../../../detection_engine/types';
-
-export interface TimelineEqlRequestOptions
- extends EqlSearchStrategyRequest,
- Omit {
- eventCategoryField?: string;
- tiebreakerField?: string;
- timestampField?: string;
- size?: number;
-}
-
-export interface TimelineEqlResponse extends EqlSearchStrategyResponse> {
- edges: TimelineEdges[];
- totalCount: number;
- pageInfo: Pick;
- inspect: Maybe;
-}
-
-export interface EqlOptionsData {
- keywordFields: EuiComboBoxOptionOption[];
- dateFields: EuiComboBoxOptionOption[];
- nonDateFields: EuiComboBoxOptionOption[];
-}
-
-export interface EqlOptionsSelected {
- eventCategoryField?: string;
- tiebreakerField?: string;
- timestampField?: string;
- query?: string;
- size?: number;
-}
-
-export type FieldsEqlOptions = keyof EqlOptionsSelected;
+export type {
+ TimelineEqlRequestOptions,
+ TimelineEqlResponse,
+ EqlOptionsData,
+ EqlOptionsSelected,
+ FieldsEqlOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
index f29dc4a3c7450..39f23a63c8afe 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
@@ -5,38 +5,11 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe } from '../../../common';
-import { TimelineRequestBasicOptions } from '../..';
-
-export enum LastEventIndexKey {
- hostDetails = 'hostDetails',
- hosts = 'hosts',
- ipDetails = 'ipDetails',
- network = 'network',
-}
-
-export interface LastTimeDetails {
- hostName?: Maybe;
- ip?: Maybe;
-}
-
-export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchResponse {
- lastSeen: Maybe;
- inspect?: Maybe;
-}
-
-export interface TimelineKpiStrategyResponse extends IEsSearchResponse {
- destinationIpCount: number;
- inspect?: Maybe;
- hostCount: number;
- processCount: number;
- sourceIpCount: number;
- userCount: number;
-}
-
-export interface TimelineEventsLastEventTimeRequestOptions
- extends Omit {
- indexKey: LastEventIndexKey;
- details: LastTimeDetails;
-}
+export { LastEventIndexKey } from '../../../../../../timelines/common';
+
+export type {
+ LastTimeDetails,
+ TimelineEventsLastEventTimeStrategyResponse,
+ TimelineKpiStrategyResponse,
+ TimelineEventsLastEventTimeRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
index 9c2c23eb334a3..7064ef033fc5a 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
@@ -24,7 +24,12 @@ import {
SortField,
Maybe,
} from '../common';
-import { DataProviderType, TimelineType, TimelineStatus } from '../../types/timeline';
+import {
+ DataProviderType,
+ TimelineType,
+ TimelineStatus,
+ RowRendererId,
+} from '../../types/timeline';
export * from './events';
@@ -165,25 +170,6 @@ export interface SortTimelineInput {
sortDirection?: Maybe;
}
-export enum RowRendererId {
- alerts = 'alerts',
- auditd = 'auditd',
- auditd_file = 'auditd_file',
- library = 'library',
- netflow = 'netflow',
- plain = 'plain',
- registry = 'registry',
- suricata = 'suricata',
- system = 'system',
- system_dns = 'system_dns',
- system_endgame_process = 'system_endgame_process',
- system_file = 'system_file',
- system_fim = 'system_fim',
- system_security_event = 'system_security_event',
- system_socket = 'system_socket',
- zeek = 'zeek',
-}
-
export interface TimelineInput {
columns?: Maybe;
dataProviders?: Maybe;
diff --git a/x-pack/plugins/index_management/public/application/components/page_error/index.ts b/x-pack/plugins/security_solution/common/types/index.ts
similarity index 80%
rename from x-pack/plugins/index_management/public/application/components/page_error/index.ts
rename to x-pack/plugins/security_solution/common/types/index.ts
index 040edfa362c63..9464a33082a49 100644
--- a/x-pack/plugins/index_management/public/application/components/page_error/index.ts
+++ b/x-pack/plugins/security_solution/common/types/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { PageErrorForbidden } from './page_error_forbidden';
+export * from './timeline';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts b/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts
new file mode 100644
index 0000000000000..782af107417c2
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/actions/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export type {
+ ActionProps,
+ HeaderActionProps,
+ GenericActionRowCellRenderProps,
+ HeaderCellRender,
+ RowCellRender,
+ ControlColumnProps,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts
new file mode 100644
index 0000000000000..83b0ced332a62
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export type { CellValueElementProps } from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts
new file mode 100644
index 0000000000000..ee4d621e35d6c
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/columns/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export type {
+ ColumnHeaderType,
+ ColumnId,
+ ColumnHeaderOptions,
+ ColumnRenderer,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts b/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts
new file mode 100644
index 0000000000000..f363176ac0a88
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export { IS_OPERATOR, EXISTS_OPERATOR } from '../../../../../timelines/common';
+
+export type {
+ QueryOperator,
+ DataProviderType,
+ QueryMatch,
+ DataProvider,
+ DataProvidersAnd,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index 7ae52a3990ff7..05cf99195774b 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -23,6 +23,13 @@ import { FlowTarget } from '../../search_strategy/security_solution/network';
import { errorSchema } from '../../detection_engine/schemas/response/error_schema';
import { Direction, Maybe } from '../../search_strategy';
+export * from './actions';
+export * from './cells';
+export * from './columns';
+export * from './data_provider';
+export * from './rows';
+export * from './store';
+
/*
* ColumnHeader Types
*/
@@ -492,6 +499,11 @@ export type TimelineExpandedDetail = {
[tab in TimelineTabs]?: TimelineExpandedDetailType;
};
+export type ToggleDetailPanel = TimelineExpandedDetailType & {
+ tabType?: TimelineTabs;
+ timelineId: string;
+};
+
export const pageInfoTimeline = runtimeTypes.type({
pageIndex: runtimeTypes.number,
pageSize: runtimeTypes.number,
diff --git a/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts b/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts
new file mode 100644
index 0000000000000..ae2d19a5e2ca8
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/rows/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export type { RowRenderer } from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts
new file mode 100644
index 0000000000000..01fc9db7c8e1d
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ColumnHeaderOptions,
+ ColumnId,
+ RowRendererId,
+ TimelineExpandedDetail,
+ TimelineTypeLiteral,
+} from '.';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { Filter } from '../../../../../../src/plugins/data/public';
+
+import { Direction } from '../../search_strategy';
+import { DataProvider } from './data_provider';
+
+export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
+
+export interface KueryFilterQuery {
+ kind: KueryFilterQueryKind;
+ expression: string;
+}
+
+export interface SerializedFilterQuery {
+ kuery: KueryFilterQuery | null;
+ serializedQuery: string;
+}
+
+export type SortDirection = 'none' | 'asc' | 'desc' | Direction;
+export interface SortColumnTimeline {
+ columnId: string;
+ columnType: string;
+ sortDirection: SortDirection;
+}
+
+export interface TimelinePersistInput {
+ id: string;
+ dataProviders?: DataProvider[];
+ dateRange?: {
+ start: string;
+ end: string;
+ };
+ excludedRowRendererIds?: RowRendererId[];
+ expandedDetail?: TimelineExpandedDetail;
+ filters?: Filter[];
+ columns: ColumnHeaderOptions[];
+ itemsPerPage?: number;
+ indexNames: string[];
+ kqlQuery?: {
+ filterQuery: SerializedFilterQuery | null;
+ };
+ show?: boolean;
+ sort?: SortColumnTimeline[];
+ showCheckboxes?: boolean;
+ timelineType?: TimelineTypeLiteral;
+ templateTimelineId?: string | null;
+ templateTimelineVersion?: number | null;
+}
+
+/** Invoked when a column is sorted */
+export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void;
+
+export type OnColumnsSorted = (
+ sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }>
+) => void;
+
+export type OnColumnRemoved = (columnId: ColumnId) => void;
+
+export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void;
+
+/** Invoked when a user clicks to load more item */
+export type OnChangePage = (nextPage: number) => void;
+
+/** Invoked when a user checks/un-checks a row */
+export type OnRowSelected = ({
+ eventIds,
+ isSelected,
+}: {
+ eventIds: string[];
+ isSelected: boolean;
+}) => void;
+
+/** Invoked when a user checks/un-checks the select all checkbox */
+export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
+
+/** Invoked when columns are updated */
+export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void;
+
+/** Invoked when a user pins an event */
+export type OnPinEvent = (eventId: string) => void;
+
+/** Invoked when a user unpins an event */
+export type OnUnPinEvent = (eventId: string) => void;
diff --git a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
index b724c0f672b50..64d4f2986903a 100644
--- a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
+++ b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
@@ -7,7 +7,7 @@
import { EventHit, EventSource } from '../search_strategy';
import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters';
-import { eventDetailsFormattedFields, eventHit } from './mock_event_details';
+import { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid';
describe('Events Details Helpers', () => {
const fields: EventHit['fields'] = eventHit.fields;
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
index f1ee0d39f545f..bf5c281a43e39 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
@@ -129,7 +129,13 @@ describe('Alerts detection rules', () => {
});
it('Auto refreshes rules', () => {
- cy.clock(Date.now());
+ /**
+ * Ran into the error: timer created with setInterval() but cleared with cancelAnimationFrame()
+ * There are no cancelAnimationFrames in the codebase that are used to clear a setInterval so
+ * explicitly set the below overrides. see https://docs.cypress.io/api/commands/clock#Function-names
+ */
+
+ cy.clock(Date.now(), ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date']);
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
index 78ee3fdcdcdd5..3ff036fa0107f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
@@ -45,7 +45,7 @@ describe('Overview Page', () => {
describe('with no data', () => {
it('Splash screen should be here', () => {
- cy.stubSearchStrategyApi(emptyInstance, undefined, 'securitySolutionIndexFields');
+ cy.stubSearchStrategyApi(emptyInstance, undefined, 'indexFields');
loginAndWaitForPage(OVERVIEW_URL);
cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts
index d42632a66eb26..a89ddf3e0b250 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts
@@ -12,6 +12,7 @@ import {
TIMELINE_DATA_PROVIDERS_ACTION_MENU,
IS_DRAGGING_DATA_PROVIDERS,
TIMELINE_FLYOUT_HEADER,
+ TIMELINE_FLYOUT,
} from '../../screens/timeline';
import { HOSTS_NAMES_DRAGGABLE } from '../../screens/hosts/all_hosts';
@@ -46,7 +47,7 @@ describe('timeline data providers', () => {
it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => {
dragAndDropFirstHostToTimeline();
openTimelineUsingToggle();
- cy.get(TIMELINE_DROPPED_DATA_PROVIDERS)
+ cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_DROPPED_DATA_PROVIDERS}`)
.first()
.invoke('text')
.then((dataProviderText) => {
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
index 568fb90568fb3..b569ea7cc082f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
@@ -12,6 +12,7 @@ import {
TIMELINE_EVENTS_COUNT_PER_PAGE_BTN,
TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION,
TIMELINE_EVENTS_COUNT_PREV_PAGE,
+ TIMELINE_FLYOUT,
} from '../../screens/timeline';
import { cleanKibana } from '../../tasks/common';
@@ -50,10 +51,10 @@ describe('Pagination', () => {
it('should be able to go to next / previous page', () => {
cy.intercept('POST', '/internal/bsearch').as('refetch');
- cy.get(TIMELINE_EVENTS_COUNT_NEXT_PAGE).first().click();
+ cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_NEXT_PAGE}`).first().click();
cy.wait('@refetch').its('response.statusCode').should('eq', 200);
- cy.get(TIMELINE_EVENTS_COUNT_PREV_PAGE).first().click();
+ cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_PREV_PAGE}`).first().click();
cy.wait('@refetch').its('response.statusCode').should('eq', 200);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
index 0a9e5b44feb1f..63c4c1364fcd0 100644
--- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
@@ -143,6 +143,8 @@ export const TIMELINE_CORRELATION_TAB = '[data-test-subj="timelineTabs-eql"]';
export const IS_DRAGGING_DATA_PROVIDERS = '.is-dragging';
+export const TIMELINE_BOTTOM_BAR_CONTAINER = '[data-test-subj="timeline-bottom-bar-container"]';
+
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
export const TIMELINE_DATA_PROVIDERS_ACTION_MENU = '[data-test-subj="providerActions"]';
@@ -175,9 +177,11 @@ export const TIMELINE_EVENTS_COUNT_PER_PAGE_BTN = '[data-test-subj="local-events
export const TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION = (itemsPerPage: number) =>
`[data-test-subj="items-per-page-option-${itemsPerPage}"]`;
-export const TIMELINE_EVENTS_COUNT_NEXT_PAGE = '[data-test-subj="pagination-button-next"]';
+export const TIMELINE_EVENTS_COUNT_NEXT_PAGE =
+ '[data-test-subj="timeline"] [data-test-subj="pagination-button-next"]';
-export const TIMELINE_EVENTS_COUNT_PREV_PAGE = '[data-test-subj="pagination-button-previous"]';
+export const TIMELINE_EVENTS_COUNT_PREV_PAGE =
+ '[data-test-subj="timeline"] [data-test-subj="pagination-button-previous"]';
export const TIMELINE_FIELDS_BUTTON =
'[data-test-subj="timeline"] [data-test-subj="show-field-browser"]';
@@ -234,7 +238,7 @@ export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]';
export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]';
-export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane-wrapper"]';
+export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane"]';
export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]';
diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js
index 90eb9a38d7509..e74d06cd621fb 100644
--- a/x-pack/plugins/security_solution/cypress/support/commands.js
+++ b/x-pack/plugins/security_solution/cypress/support/commands.js
@@ -35,7 +35,7 @@ Cypress.Commands.add(
'stubSearchStrategyApi',
function (stubObject, factoryQueryType, searchStrategyName = 'securitySolutionSearchStrategy') {
cy.intercept('POST', '/internal/bsearch', (req) => {
- if (searchStrategyName === 'securitySolutionIndexFields') {
+ if (searchStrategyName === 'indexFields') {
req.reply(stubObject.rawResponse);
} else if (factoryQueryType === 'overviewHost') {
req.reply(stubObject.overviewHost);
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index 02dbc56bd3397..e26f0d9b65bfa 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -17,6 +17,7 @@
"inspector",
"licensing",
"maps",
+ "timelines",
"triggersActionsUi",
"uiActions"
],
diff --git a/x-pack/plugins/security_solution/public/app/404.tsx b/x-pack/plugins/security_solution/public/app/404.tsx
index c21f7a4d4d578..2634ffd47bff1 100644
--- a/x-pack/plugins/security_solution/public/app/404.tsx
+++ b/x-pack/plugins/security_solution/public/app/404.tsx
@@ -8,15 +8,15 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { WrapperPage } from '../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
export const NotFoundPage = React.memo(() => (
-
+
-
+
));
NotFoundPage.displayName = 'NotFoundPage';
diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx
index cfb25c4436db3..c223570c77201 100644
--- a/x-pack/plugins/security_solution/public/app/app.tsx
+++ b/x-pack/plugins/security_solution/public/app/app.tsx
@@ -11,7 +11,7 @@ import { Store, Action } from 'redux';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { EuiErrorBoundary } from '@elastic/eui';
-import { AppLeaveHandler } from '../../../../../src/core/public';
+import { AppLeaveHandler, AppMountParameters } from '../../../../../src/core/public';
import { ManageUserInfo } from '../detections/components/user_info';
import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants';
@@ -21,7 +21,6 @@ import { GlobalToaster, ManageGlobalToaster } from '../common/components/toaster
import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana';
import { State } from '../common/store';
-import { ManageGlobalTimeline } from '../timelines/components/manage_timeline';
import { StartServices } from '../types';
import { PageRouter } from './routes';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
@@ -31,10 +30,17 @@ interface StartAppComponent {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
}
-const StartAppComponent: FC = ({ children, history, onAppLeave, store }) => {
+const StartAppComponent: FC = ({
+ children,
+ history,
+ setHeaderActionMenu,
+ onAppLeave,
+ store,
+}) => {
const { i18n } = useKibana().services;
const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE);
@@ -42,23 +48,25 @@ const StartAppComponent: FC = ({ children, history, onAppLeav
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
@@ -72,6 +80,7 @@ interface SecurityAppComponentProps {
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
services: StartServices;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
}
@@ -80,6 +89,7 @@ const SecurityAppComponent: React.FC = ({
history,
onAppLeave,
services,
+ setHeaderActionMenu,
store,
}) => (
= ({
...services,
}}
>
-
+
{children}
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
new file mode 100644
index 0000000000000..98ff11423ce01
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import {
+ EuiHeaderSection,
+ EuiHeaderLinks,
+ EuiHeaderLink,
+ EuiHeaderSectionItem,
+} from '@elastic/eui';
+import React, { useEffect, useMemo } from 'react';
+import { createPortalNode, OutPortal, InPortal } from 'react-reverse-portal';
+import { i18n } from '@kbn/i18n';
+
+import { AppMountParameters } from '../../../../../../../src/core/public';
+import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
+import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
+import { useKibana } from '../../../common/lib/kibana';
+import { ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
+
+const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', {
+ defaultMessage: 'Add data',
+});
+
+/**
+ * This component uses the reverse portal to add the Add Data and ML job settings buttons on the
+ * right hand side of the Kibana global header
+ */
+export const GlobalHeader = React.memo(
+ ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
+ const portalNode = useMemo(() => createPortalNode(), []);
+ const { http } = useKibana().services;
+
+ useEffect(() => {
+ let unmount = () => {};
+
+ setHeaderActionMenu((element) => {
+ const mount = toMountPoint( );
+ unmount = mount(element);
+ return unmount;
+ });
+
+ return () => {
+ portalNode.unmount();
+ unmount();
+ };
+ }, [portalNode, setHeaderActionMenu]);
+
+ return (
+
+
+ {window.location.pathname.includes(APP_DETECTIONS_PATH) && (
+
+
+
+ )}
+
+
+
+ {BUTTON_ADD_DATA}
+
+
+
+
+
+ );
+ }
+);
+GlobalHeader.displayName = 'GlobalHeader';
diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
index 7ebcc96753836..8358e2f9377b8 100644
--- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import * as i18n from './translations';
+import * as i18n from '../translations';
import { SecurityPageName } from '../types';
import { SiemNavTab } from '../../common/components/navigation/types';
import {
diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx
index 1b0ddcfb9ae7d..9a57ab3fc3a73 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.tsx
@@ -5,57 +5,35 @@
* 2.0.
*/
-import React, { useEffect, useRef, useState } from 'react';
-import styled from 'styled-components';
+import React, { useRef } from 'react';
-import { TimelineId } from '../../../common/types/timeline';
import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper';
-import { Flyout } from '../../timelines/components/flyout';
+import { AppLeaveHandler, AppMountParameters } from '../../../../../../src/core/public';
import { SecuritySolutionAppWrapper } from '../../common/components/page';
-import { HeaderGlobal } from '../../common/components/header_global';
import { HelpMenu } from '../../common/components/help_menu';
-import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning';
import { UseUrlState } from '../../common/components/url_state';
-import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline';
import { navTabs } from './home_navigations';
import { useInitSourcerer, useSourcererScope } from '../../common/containers/sourcerer';
import { useKibana } from '../../common/lib/kibana';
import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useUpgradeEndpointPackage } from '../../common/hooks/endpoint/upgrade';
-import { useThrottledResizeObserver } from '../../common/components/utils';
-import { AppLeaveHandler } from '../../../../../../src/core/public';
-
-const Main = styled.main.attrs<{ paddingTop: number }>(({ paddingTop }) => ({
- style: {
- paddingTop: `${paddingTop}px`,
- },
-}))<{ paddingTop: number }>`
- overflow: auto;
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
-`;
-
-Main.displayName = 'Main';
+import { GlobalHeader } from './global_header';
+import { SecuritySolutionTemplateWrapper } from './template_wrapper';
interface HomePageProps {
children: React.ReactNode;
onAppLeave: (handler: AppLeaveHandler) => void;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const HomePageComponent: React.FC = ({ children, onAppLeave }) => {
- const { application, overlays } = useKibana().services;
+const HomePageComponent: React.FC = ({
+ children,
+ onAppLeave,
+ setHeaderActionMenu,
+}) => {
+ const { application } = useKibana().services;
const subPluginId = useRef('');
- const { ref, height = 0 } = useThrottledResizeObserver(300);
- const banners$ = overlays.banners.get$();
- const [headerFixed, setHeaderFixed] = useState(true);
- const mainPaddingTop = headerFixed ? height : 0;
-
- useEffect(() => {
- const subscription = banners$.subscribe((banners) => setHeaderFixed(!banners.length));
- return () => subscription.unsubscribe();
- }, [banners$]); // Only un/re-subscribe if the Observable changes
application.currentAppId$.subscribe((appId) => {
subPluginId.current = appId ?? '';
@@ -66,13 +44,13 @@ const HomePageComponent: React.FC = ({ children, onAppLeave }) =>
? SourcererScopeName.detections
: SourcererScopeName.default
);
- const [showTimeline] = useShowTimeline();
- const { browserFields, indexPattern, indicesExist } = useSourcererScope(
+ const { browserFields, indexPattern } = useSourcererScope(
subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
? SourcererScopeName.detections
: SourcererScopeName.default
);
+
// side effect: this will attempt to upgrade the endpoint package if it is not up to date
// this will run when a user navigates to the Security Solution app and when they navigate between
// tabs in the app. This is useful for keeping the endpoint package as up to date as possible until
@@ -81,23 +59,14 @@ const HomePageComponent: React.FC = ({ children, onAppLeave }) =>
useUpgradeEndpointPackage();
return (
-
-
-
-
-
-
- {indicesExist && showTimeline && (
- <>
-
-
- >
- )}
-
+
+
+
+
+
{children}
-
-
-
+
+
);
diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx
new file mode 100644
index 0000000000000..08ebbeaee55d4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable react/display-name */
+
+import React, { useRef } from 'react';
+import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public';
+import { AppLeaveHandler } from '../../../../../../../../src/core/public';
+import { useKibana } from '../../../../common/lib/kibana';
+import { useShowTimeline } from '../../../../common/utils/timeline/use_show_timeline';
+import { useSourcererScope } from '../../../../common/containers/sourcerer';
+import { DETECTIONS_SUB_PLUGIN_ID } from '../../../../../common/constants';
+import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
+import { TimelineId } from '../../../../../common/types/timeline';
+import { AutoSaveWarningMsg } from '../../../../timelines/components/timeline/auto_save_warning';
+import { Flyout } from '../../../../timelines/components/flyout';
+
+export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar';
+
+export const SecuritySolutionBottomBar = React.memo(
+ ({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => {
+ const subPluginId = useRef('');
+ const { application } = useKibana().services;
+ application.currentAppId$.subscribe((appId) => {
+ subPluginId.current = appId ?? '';
+ });
+
+ const [showTimeline] = useShowTimeline();
+
+ const { indicesExist } = useSourcererScope(
+ subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
+ ? SourcererScopeName.detections
+ : SourcererScopeName.default
+ );
+
+ return indicesExist && showTimeline ? (
+ <>
+
+
+ >
+ ) : null;
+ }
+);
+
+export const SecuritySolutionBottomBarProps: KibanaPageTemplateProps['bottomBarProps'] = {
+ className: BOTTOM_BAR_CLASSNAME,
+ 'data-test-subj': 'timeline-bottom-bar-container',
+ position: 'fixed',
+ usePortal: false,
+};
diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx
new file mode 100644
index 0000000000000..3e3c91133eab6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import styled from 'styled-components';
+import { OutPortal } from 'react-reverse-portal';
+import { useGlobalHeaderPortal } from '../../../../common/hooks/use_global_header_portal';
+
+const StyledStickyWrapper = styled.div`
+ position: sticky;
+ z-index: ${(props) => props.theme.eui.euiZLevel2};
+ // TOP location is declared in src/public/rendering/_base.scss to keep in line with Kibana Chrome
+`;
+
+export const GlobalKQLHeader = React.memo(() => {
+ const { globalKQLHeaderPortalNode } = useGlobalHeaderPortal();
+
+ return (
+
+
+
+ );
+});
+
+GlobalKQLHeader.displayName = 'GlobalKQLHeader';
diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx
new file mode 100644
index 0000000000000..02fd07151f111
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import styled from 'styled-components';
+import { EuiPanel } from '@elastic/eui';
+import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
+import { AppLeaveHandler } from '../../../../../../../src/core/public';
+import { KibanaPageTemplate } from '../../../../../../../src/plugins/kibana_react/public';
+import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation';
+import { TimelineId } from '../../../../common/types/timeline';
+import { getTimelineShowStatusByIdSelector } from '../../../timelines/components/flyout/selectors';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
+import { GlobalKQLHeader } from './global_kql_header';
+import {
+ BOTTOM_BAR_CLASSNAME,
+ SecuritySolutionBottomBar,
+ SecuritySolutionBottomBarProps,
+} from './bottom_bar';
+import { useShowTimeline } from '../../../common/utils/timeline/use_show_timeline';
+import { gutterTimeline } from '../../../common/lib/helpers';
+
+/* eslint-disable react/display-name */
+
+/**
+ * Need to apply the styles via a className to effect the containing bottom bar
+ * rather than applying them to the timeline bar directly
+ */
+const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{
+ $isShowingTimelineOverlay?: boolean;
+ $isTimelineBottomBarVisible?: boolean;
+}>`
+ .${BOTTOM_BAR_CLASSNAME} {
+ animation: 'none !important'; // disable the default bottom bar slide animation
+ background: ${({ theme }) =>
+ theme.eui.euiColorEmptyShade}; // Override bottom bar black background
+ color: inherit; // Necessary to override the bottom bar 'white text'
+ transform: ${(
+ { $isShowingTimelineOverlay } // Since the bottom bar wraps the whole overlay now, need to override any transforms when it is open
+ ) => ($isShowingTimelineOverlay ? 'none' : 'translateY(calc(100% - 50px))')};
+ z-index: ${({ theme }) => theme.eui.euiZLevel8};
+
+ .${IS_DRAGGING_CLASS_NAME} & {
+ // When a drag is in process the bottom flyout should slide up to allow a drop
+ transform: none;
+ }
+ }
+
+ // If the bottom bar is visible add padding to the navigation
+ ${({ $isTimelineBottomBarVisible }) =>
+ $isTimelineBottomBarVisible &&
+ `
+ @media (min-width: 768px) {
+ .kbnPageTemplateSolutionNav {
+ padding-bottom: ${gutterTimeline};
+ }
+ }
+ `}
+`;
+
+interface SecuritySolutionPageWrapperProps {
+ onAppLeave: (handler: AppLeaveHandler) => void;
+}
+
+export const SecuritySolutionTemplateWrapper: React.FC = React.memo(
+ ({ children, onAppLeave }) => {
+ const solutionNav = useSecuritySolutionNavigation();
+ const [isTimelineBottomBarVisible] = useShowTimeline();
+ const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
+ const { show: isShowingTimelineOverlay } = useDeepEqualSelector((state) =>
+ getTimelineShowStatus(state, TimelineId.active)
+ );
+
+ return (
+ }
+ paddingSize="none"
+ solutionNav={solutionNav}
+ restrictWidth={false}
+ template="default"
+ >
+
+
+ {children}
+
+
+ );
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx
index 1e304c2686960..194f119e35478 100644
--- a/x-pack/plugins/security_solution/public/app/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/index.tsx
@@ -15,12 +15,19 @@ export const renderApp = ({
element,
history,
onAppLeave,
+ setHeaderActionMenu,
services,
store,
SubPluginRoutes,
}: RenderAppProps): (() => void) => {
render(
-
+
,
element
diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx
index 6454653af5214..a9a94a6998286 100644
--- a/x-pack/plugins/security_solution/public/app/routes.tsx
+++ b/x-pack/plugins/security_solution/public/app/routes.tsx
@@ -10,7 +10,7 @@ import React, { FC, memo, useEffect } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { useDispatch } from 'react-redux';
-import { AppLeaveHandler } from '../../../../../src/core/public';
+import { AppLeaveHandler, AppMountParameters } from '../../../../../src/core/public';
import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes';
import { RouteCapture } from '../common/components/endpoint/route_capture';
import { AppAction } from '../common/store/actions';
@@ -21,9 +21,15 @@ interface RouterProps {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const PageRouterComponent: FC = ({ children, history, onAppLeave }) => {
+const PageRouterComponent: FC = ({
+ children,
+ history,
+ onAppLeave,
+ setHeaderActionMenu,
+}) => {
const dispatch = useDispatch<(action: AppAction) => void>();
useEffect(() => {
return () => {
@@ -42,7 +48,9 @@ const PageRouterComponent: FC = ({ children, history, onAppLeave })
- {children}
+
+ {children}
+
diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/app/home/translations.ts
rename to x-pack/plugins/security_solution/public/app/translations.ts
diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx
index caa88a8fd1c2a..d22aafa450694 100644
--- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx
@@ -50,7 +50,7 @@ describe('CreateCaseFlyout', () => {
);
- wrapper.find('.euiFlyout__closeButton').first().simulate('click');
+ wrapper.find(`[data-test-subj='euiFlyoutCloseButton']`).first().simulate('click');
expect(onCloseFlyout).toBeCalled();
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx
index 1023bfc8b0206..f01ae342f8547 100644
--- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx
@@ -5,8 +5,8 @@
* 2.0.
*/
-import React, { memo } from 'react';
-import styled from 'styled-components';
+import React, { memo, ReactNode } from 'react';
+import styled, { StyledComponent } from 'styled-components';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import * as i18n from '../../translations';
@@ -20,7 +20,11 @@ export interface CreateCaseModalProps {
onSuccess: (theCase: Case) => Promise;
}
-const StyledFlyout = styled(EuiFlyout)`
+// TODO: EUI team follow up on complex types and styled-components `styled`
+// https://github.com/elastic/eui/issues/4855
+const StyledFlyout: StyledComponent = styled(
+ EuiFlyout
+)`
${({ theme }) => `
z-index: ${theme.eui.euiZModal};
`}
diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
index 91fb45de04320..dfd53ae5cc0b0 100644
--- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
@@ -38,7 +38,7 @@ export const Create = React.memo(() => {
);
return (
-
+
{cases.getCreateCase({
onCancel: handleSetIsCancel,
onSuccess,
diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx
index 647647afbe0a4..ad0176bda6905 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx
@@ -7,7 +7,7 @@
import React from 'react';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useGetUserCasesPermissions } from '../../common/lib/kibana';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { AllCases } from '../components/all_cases';
@@ -20,9 +20,9 @@ export const CasesPage = React.memo(() => {
return userPermissions == null || userPermissions?.read ? (
<>
-
+
-
+
>
) : (
diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
index a086409e55df5..f6bb27b7b7104 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
@@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom';
import { SecurityPageName } from '../../app/types';
import { SpyRoute } from '../../common/utils/route/spy_routes';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search';
import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
import { getCaseUrl } from '../../common/components/link_to';
@@ -37,13 +37,13 @@ export const CaseDetailsPage = React.memo(() => {
return caseId != null ? (
<>
-
+
-
+
>
) : null;
diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx
index c942065e45278..d3f235a5da7dc 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx
@@ -11,7 +11,7 @@ import styled from 'styled-components';
import { SecurityPageName } from '../../app/types';
import { getCaseUrl } from '../../common/components/link_to';
import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { navTabs } from '../../app/home/home_navigations';
@@ -51,7 +51,7 @@ const ConfigureCasesPageComponent: React.FC = () => {
return (
<>
-
+
@@ -63,7 +63,7 @@ const ConfigureCasesPageComponent: React.FC = () => {
owner: [APP_ID],
})}
-
+
>
);
diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
index 3c5197f19eff1..6c88c4afb6395 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
@@ -10,7 +10,7 @@ import React, { useEffect, useMemo } from 'react';
import { SecurityPageName } from '../../app/types';
import { getCaseUrl } from '../../common/components/link_to';
import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { navTabs } from '../../app/home/home_navigations';
@@ -45,10 +45,10 @@ export const CreateCasePage = React.memo(() => {
return (
<>
-
+
-
+
>
);
diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts b/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts
new file mode 100644
index 0000000000000..f05644c85e536
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './tooltip_with_keyboard_shortcut';
diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx
index 97922ecdc5b61..2d66b4e93e4dc 100644
--- a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import * as i18n from './translations';
-interface Props {
+export interface TooltipWithKeyboardShortcutProps {
additionalScreenReaderOnlyContext?: string;
content: React.ReactNode;
shortcut: string;
@@ -22,7 +22,7 @@ const TooltipWithKeyboardShortcutComponent = ({
content,
shortcut,
showShortcut,
-}: Props) => (
+}: TooltipWithKeyboardShortcutProps) => (
<>
{content}
{additionalScreenReaderOnlyContext !== '' && (
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
index 43d5c66655808..58cca7bcbd121 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
@@ -6,12 +6,12 @@
*/
import React, { useEffect, useMemo } from 'react';
-
+import { useDispatch } from 'react-redux';
+import { timelineActions } from '../../../timelines/store/timeline';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../events_viewer';
import { alertsDefaultModel } from './default_headers';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import * as i18n from './translations';
@@ -70,22 +70,24 @@ const AlertsTableComponent: React.FC = ({
startDate,
pageFilters = [],
}) => {
+ const dispatch = useDispatch();
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
const { filterManager } = useKibana().services.data.query;
- const { initializeTimeline } = useManageTimeline();
useEffect(() => {
- initializeTimeline({
- id: timelineId,
- documentType: i18n.ALERTS_DOCUMENT_TYPE,
- filterManager,
- defaultModel: alertsDefaultModel,
- footerText: i18n.TOTAL_COUNT_OF_ALERTS,
- title: i18n.ALERTS_TABLE_TITLE,
- unit: i18n.UNIT,
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ id: timelineId,
+ documentType: i18n.ALERTS_DOCUMENT_TYPE,
+ filterManager,
+ defaultColumns: alertsDefaultModel.columns,
+ excludedRowRendererIds: alertsDefaultModel.excludedRowRendererIds,
+ footerText: i18n.TOTAL_COUNT_OF_ALERTS,
+ title: i18n.ALERTS_TABLE_TITLE,
+ // TODO: avoid passing this through the store
+ })
+ );
+ }, [dispatch, filterManager, timelineId]);
return (
= ({ namespace, conditi
const { isVisible, dismiss } = useCallOutStorage([message], namespace);
const shouldRender = condition && isVisible(message);
- return shouldRender ? : null;
+ return shouldRender ? (
+ <>
+
+
+ >
+ ) : null;
};
export const CallOutSwitcher = memo(CallOutSwitcherComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
index 544f9b1abf8f2..a01f22a0942de 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
@@ -15,6 +15,8 @@ import { TestProviders } from '../../mock';
import { MIN_LEGEND_HEIGHT, DraggableLegend } from './draggable_legend';
import { LegendItem } from './draggable_legend_item';
+jest.mock('../../lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
index 4958f6bae4a30..175239fcaebe7 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
@@ -14,6 +14,8 @@ import { TestProviders } from '../../mock';
import { DraggableLegendItem, LegendItem } from './draggable_legend_item';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
index dc0e24fcba8f5..bc3b9c3eaa1c6 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../mock';
import { DragDropContextWrapper } from './drag_drop_context_wrapper';
+jest.mock('../../lib/kibana');
+
describe('DragDropContextWrapper', () => {
describe('rendering', () => {
test('it renders against the snapshot', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx
index 1073ed57dee19..1ab19c44e29b2 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx
@@ -11,6 +11,7 @@ import { DropResult, DragDropContext } from 'react-beautiful-dnd';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import deepEqual from 'fast-deep-equal';
+import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { BeforeCapture } from './drag_drop_context';
import { BrowserFields } from '../../containers/source';
@@ -23,22 +24,24 @@ import {
ADDED_TO_TIMELINE_MESSAGE,
ADDED_TO_TIMELINE_TEMPLATE_MESSAGE,
} from '../../hooks/translations';
-import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline';
import { displaySuccessToast, useStateToaster } from '../toasters';
import { TimelineId, TimelineType } from '../../../../common/types/timeline';
import {
- addFieldToTimelineColumns,
addProviderToTimeline,
fieldWasDroppedOnTimelineColumns,
- getTimelineIdFromColumnDroppableId,
- IS_DRAGGING_CLASS_NAME,
IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME,
providerWasDroppedOnTimeline,
draggableIsField,
userIsReArrangingProviders,
} from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
+import { useKibana } from '../../lib/kibana';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
+import {
+ addFieldToTimelineColumns,
+ getTimelineIdFromColumnDroppableId,
+} from '../../../../../timelines/public';
+import { alertsHeaders } from '../alerts_viewer/default_headers';
// @ts-expect-error
window['__react-beautiful-dnd-disable-dev-warnings'] = true;
@@ -85,6 +88,7 @@ const onDragEndHandler = ({
} else if (fieldWasDroppedOnTimelineColumns(result)) {
addFieldToTimelineColumns({
browserFields,
+ defaultsHeader: alertsHeaders,
dispatch,
result,
timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''),
@@ -92,8 +96,6 @@ const onDragEndHandler = ({
}
};
-const sensors = [useAddToTimelineSensor];
-
/**
* DragDropContextWrapperComponent handles all drag end events
*/
@@ -101,7 +103,8 @@ export const DragDropContextWrapperComponent: React.FC = ({ browserFields
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const getDataProviders = useMemo(() => dragAndDropSelectors.getDataProvidersSelector(), []);
-
+ const { timelines } = useKibana().services;
+ const sensors = [timelines.getUseAddToTimelineSensor()];
const {
dataProviders: activeTimelineDataProviders,
timelineType,
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
index 0d8011ee8b65d..bdc5545880e1c 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
@@ -17,6 +17,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper';
import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
index 0cb030862a389..9db5b3899d8bc 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
@@ -6,6 +6,7 @@
*/
import { EuiScreenReaderOnly } from '@elastic/eui';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import {
Draggable,
@@ -24,12 +25,12 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/com
import { TruncatableText } from '../truncatable_text';
import { WithHoverActions } from '../with_hover_actions';
-import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook';
import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content';
-import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, getDraggableId, getDroppableId } from './helpers';
+import { getDraggableId, getDroppableId } from './helpers';
import { ProviderContainer } from './provider_container';
import * as i18n from './translations';
+import { useKibana } from '../../lib/kibana';
// As right now, we do not know what we want there, we will keep it as a placeholder
export const DragEffects = styled.div``;
@@ -142,6 +143,7 @@ const DraggableWrapperComponent: React.FC = ({
const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false);
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
@@ -297,7 +299,7 @@ const DraggableWrapperComponent: React.FC = ({
setHoverActionsOwnFocus(true);
}, []);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId: getDraggableId(dataProvider.id),
fieldName: dataProvider.queryMatch.field,
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
index 0d688bd805999..400b178c167f6 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
@@ -17,14 +17,10 @@ import { TestProviders } from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { useSourcererScope } from '../../containers/sourcerer';
import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content';
-import {
- ManageGlobalTimeline,
- getTimelineDefaults,
-} from '../../../timelines/components/manage_timeline';
import { TimelineId } from '../../../../common/types/timeline';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
jest.mock('../link_to');
-
jest.mock('../../lib/kibana');
jest.mock('../../containers/sourcerer', () => {
const original = jest.requireActual('../../containers/sourcerer');
@@ -42,29 +38,18 @@ jest.mock('uuid', () => {
};
});
const mockStartDragToTimeline = jest.fn();
-jest.mock('../../hooks/use_add_to_timeline', () => {
- const original = jest.requireActual('../../hooks/use_add_to_timeline');
+jest.mock('../../../../../timelines/public/hooks/use_add_to_timeline', () => {
+ const original = jest.requireActual('../../../../../timelines/public/hooks/use_add_to_timeline');
return {
...original,
useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }),
};
});
const mockAddFilters = jest.fn();
-const mockGetTimelineFilterManager = jest.fn().mockReturnValue({
- addFilters: mockAddFilters,
-});
-jest.mock('../../../timelines/components/manage_timeline', () => {
- const original = jest.requireActual('../../../timelines/components/manage_timeline');
-
- return {
- ...original,
- useManageTimeline: () => ({
- getManageTimelineById: jest.fn().mockReturnValue({ indexToAdd: [] }),
- getTimelineFilterManager: mockGetTimelineFilterManager,
- isManagedTimeline: jest.fn().mockReturnValue(false),
- }),
- };
-});
+jest.mock('../../../common/hooks/use_selector', () => ({
+ useShallowEqualSelector: jest.fn(),
+ useDeepEqualSelector: jest.fn(),
+}));
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
const timelineId = TimelineId.active;
@@ -85,6 +70,9 @@ const defaultProps = {
describe('DraggableWrapperHoverContent', () => {
beforeAll(() => {
mockStartDragToTimeline.mockReset();
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({
+ filterManager: { addFilters: mockAddFilters },
+ });
(useSourcererScope as jest.Mock).mockReturnValue({
browserFields: mockBrowserFields,
selectedPatterns: [],
@@ -144,15 +132,10 @@ describe('DraggableWrapperHoverContent', () => {
beforeEach(() => {
onFilterAdded = jest.fn();
- const manageTimelineForTesting = {
- [timelineId]: getTimelineDefaults(timelineId),
- };
wrapper = mount(
-
-
-
+
);
});
@@ -237,18 +220,9 @@ describe('DraggableWrapperHoverContent', () => {
filterManager.addFilters = jest.fn();
onFilterAdded = jest.fn();
- const manageTimelineForTesting = {
- [timelineId]: {
- ...getTimelineDefaults(timelineId),
- filterManager,
- },
- };
-
wrapper = mount(
-
-
-
+
);
});
@@ -586,39 +560,4 @@ describe('DraggableWrapperHoverContent', () => {
expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false);
});
});
-
- describe('Filter Manager', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
- test('filter manager, not active timeline', () => {
- mount(
-
-
-
- );
-
- expect(mockGetTimelineFilterManager).not.toBeCalled();
- });
- test('filter manager, active timeline', () => {
- mount(
-
-
-
- );
-
- expect(mockGetTimelineFilterManager).toBeCalled();
- });
- test('filter manager, active timeline in draggableId', () => {
- mount(
-
-
-
- );
-
- expect(mockGetTimelineFilterManager).toBeCalled();
- });
- });
});
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
index 880f0b4e18aca..71c3114015a03 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
@@ -12,14 +12,12 @@ import {
EuiScreenReaderOnly,
EuiToolTip,
} from '@elastic/eui';
+
import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react';
import { DraggableId } from 'react-beautiful-dnd';
import styled from 'styled-components';
-import { stopPropagationAndPreventDefault } from '../accessibility/helpers';
-import { TooltipWithKeyboardShortcut } from '../accessibility/tooltip_with_keyboard_shortcut';
import { getAllFieldsByName } from '../../containers/source';
-import { useAddToTimeline } from '../../hooks/use_add_to_timeline';
import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
import { useKibana } from '../../lib/kibana';
@@ -28,11 +26,14 @@ import { StatefulTopN } from '../top_n';
import { allowTopN } from './helpers';
import * as i18n from './translations';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
+import { useDeepEqualSelector } from '../../hooks/use_selector';
import { TimelineId } from '../../../../common/types/timeline';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
+import { timelineSelectors } from '../../../timelines/store/timeline';
+import { stopPropagationAndPreventDefault } from '../../../../../timelines/public';
+import { TooltipWithKeyboardShortcut } from '../accessibility';
export const AdditionalContent = styled.div`
padding: 2px;
@@ -102,21 +103,25 @@ const DraggableWrapperHoverContentComponent: React.FC = ({
toggleTopN,
value,
}) => {
- const { startDragToTimeline } = useAddToTimeline({ draggableId, fieldName: field });
const kibana = useKibana();
+ const { timelines } = kibana.services;
+ const { startDragToTimeline } = timelines.getUseAddToTimeline()({
+ draggableId,
+ fieldName: field,
+ });
const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [
kibana.services.data.query.filterManager,
]);
- const { getTimelineFilterManager } = useManageTimeline();
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, timelineId ?? '')
+ );
const defaultFocusedButtonRef = useRef(null);
const panelRef = useRef(null);
const filterManager = useMemo(
- () =>
- timelineId === TimelineId.active
- ? getTimelineFilterManager(TimelineId.active)
- : filterManagerBackup,
- [timelineId, getTimelineFilterManager, filterManagerBackup]
+ () => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup),
+ [timelineId, activeFilterMananager, filterManagerBackup]
);
// Regarding data from useManageTimeline:
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
index 42f70e9d296b3..73a732b5d6458 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
@@ -15,6 +15,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper';
import { DroppableWrapper } from './droppable_wrapper';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
describe('DroppableWrapper', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts
index 58d2e0e7dc70f..a14a44cd9a68b 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts
@@ -7,6 +7,7 @@
import { omit } from 'lodash/fp';
import { DropResult } from 'react-beautiful-dnd';
+import { getTimelineIdFromColumnDroppableId } from '../../../../../timelines/public';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
@@ -33,7 +34,6 @@ import {
getDroppableId,
getFieldIdFromDraggable,
getProviderIdFromDraggable,
- getTimelineIdFromColumnDroppableId,
getTimelineProviderDraggableId,
getTimelineProviderDroppableId,
providerWasDroppedOnTimeline,
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
index e2e506e6e1a3f..9717e1e1eda91 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
@@ -4,138 +4,53 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import { isString } from 'lodash/fp';
-import { DropResult, FluidDragActions, Position } from 'react-beautiful-dnd';
+import { DropResult } from 'react-beautiful-dnd';
import { Dispatch } from 'redux';
import { ActionCreator } from 'typescript-fsa';
+import { getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid';
-import { stopPropagationAndPreventDefault } from '../accessibility/helpers';
-import { alertsHeaders } from '../alerts_viewer/default_headers';
-import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source';
+import { BrowserField } from '../../containers/source';
import { dragAndDropActions } from '../../store/actions';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
-import { timelineActions } from '../../../timelines/store/timeline';
-import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
-import { TimelineId } from '../../../../common/types/timeline';
-
-export const draggableIdPrefix = 'draggableId';
-
-export const droppableIdPrefix = 'droppableId';
-
-export const draggableContentPrefix = `${draggableIdPrefix}.content.`;
-
-export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`;
-
-export const draggableFieldPrefix = `${draggableIdPrefix}.field.`;
-
-export const droppableContentPrefix = `${droppableIdPrefix}.content.`;
-
-export const droppableFieldPrefix = `${droppableIdPrefix}.field.`;
-
-export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`;
-
-export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`;
-
-export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`;
-
-export const getDraggableId = (dataProviderId: string): string =>
- `${draggableContentPrefix}${dataProviderId}`;
-
-export const getDraggableFieldId = ({
- contextId,
- fieldId,
-}: {
- contextId: string;
- fieldId: string;
-}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`;
-
-export const getTimelineProviderDroppableId = ({
- groupIndex,
- timelineId,
-}: {
- groupIndex: number;
- timelineId: string;
-}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`;
-
-export const getTimelineProviderDraggableId = ({
- dataProviderId,
- groupIndex,
- timelineId,
-}: {
- dataProviderId: string;
- groupIndex: number;
- timelineId: string;
-}): string =>
- `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`;
-
-export const getDroppableId = (visualizationPlaceholderId: string): string =>
- `${droppableContentPrefix}${visualizationPlaceholderId}`;
-
-export const sourceIsContent = (result: DropResult): boolean =>
- result.source.droppableId.startsWith(droppableContentPrefix);
-
-export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => {
- const regex = /^droppableId\.timelineProviders\.(\S+)\./;
- const sourceMatches = result.source.droppableId.match(regex) ?? [];
- const destinationMatches = result.destination?.droppableId.match(regex) ?? [];
-
- return (
- sourceMatches.length >= 2 &&
- destinationMatches.length >= 2 &&
- sourceMatches[1] === destinationMatches[1]
- );
-};
-
-export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
- result.draggableId.startsWith(draggableContentPrefix);
-
-export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
- result.draggableId.startsWith(draggableFieldPrefix);
-
-export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
-
-export const destinationIsTimelineProviders = (result: DropResult): boolean =>
- result.destination != null &&
- result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix);
-
-export const destinationIsTimelineColumns = (result: DropResult): boolean =>
- result.destination != null &&
- result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix);
-
-export const destinationIsTimelineButton = (result: DropResult): boolean =>
- result.destination != null &&
- result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix);
-
-export const getProviderIdFromDraggable = (result: DropResult): string =>
- result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1);
-
-export const getFieldIdFromDraggable = (result: DropResult): string =>
- unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1));
-
-export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_');
-
-export const escapeContextId = (path: string) => path.replace(/\./g, '_');
-
-export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!');
-
-export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.');
-
-export const providerWasDroppedOnTimeline = (result: DropResult): boolean =>
- reasonIsDrop(result) &&
- draggableIsContent(result) &&
- sourceIsContent(result) &&
- destinationIsTimelineProviders(result);
-
-export const userIsReArrangingProviders = (result: DropResult): boolean =>
- reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result);
-
-export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean =>
- reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result);
+export {
+ draggableIdPrefix,
+ droppableIdPrefix,
+ draggableContentPrefix,
+ draggableTimelineProvidersPrefix,
+ draggableFieldPrefix,
+ draggableIsField,
+ droppableContentPrefix,
+ droppableFieldPrefix,
+ droppableTimelineProvidersPrefix,
+ droppableTimelineColumnsPrefix,
+ droppableTimelineFlyoutBottomBarPrefix,
+ getDraggableId,
+ getDraggableFieldId,
+ getTimelineProviderDroppableId,
+ getTimelineProviderDraggableId,
+ getDroppableId,
+ sourceIsContent,
+ sourceAndDestinationAreSameTimelineProviders,
+ draggableIsContent,
+ reasonIsDrop,
+ destinationIsTimelineProviders,
+ destinationIsTimelineColumns,
+ destinationIsTimelineButton,
+ getProviderIdFromDraggable,
+ getFieldIdFromDraggable,
+ escapeDataProviderId,
+ escapeContextId,
+ escapeFieldId,
+ unEscapeFieldId,
+ providerWasDroppedOnTimeline,
+ userIsReArrangingProviders,
+ fieldWasDroppedOnTimelineColumns,
+ DRAG_TYPE_FIELD,
+ IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME,
+} from '@kbn/securitysolution-t-grid';
interface AddProviderToTimelineParams {
activeTimelineDataProviders: DataProvider[];
dataProviders: IdToDataProvider;
@@ -148,18 +63,6 @@ interface AddProviderToTimelineParams {
timelineId: string;
}
-interface AddFieldToTimelineColumnsParams {
- upsertColumn?: ActionCreator<{
- column: ColumnHeaderOptions;
- id: string;
- index: number;
- }>;
- browserFields: BrowserFields;
- dispatch: Dispatch;
- result: DropResult;
- timelineId: string;
-}
-
export const addProviderToTimeline = ({
activeTimelineDataProviders,
dataProviders,
@@ -186,73 +89,6 @@ export const addProviderToTimeline = ({
}
};
-const linkFields: Record = {
- 'signal.rule.name': 'signal.rule.id',
- 'event.module': 'rule.reference',
-};
-
-export const addFieldToTimelineColumns = ({
- upsertColumn = timelineActions.upsertColumn,
- browserFields,
- dispatch,
- result,
- timelineId,
-}: AddFieldToTimelineColumnsParams): void => {
- const fieldId = getFieldIdFromDraggable(result);
- const allColumns = getAllFieldsByName(browserFields);
- const column = allColumns[fieldId];
- const initColumnHeader =
- timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage
- ? alertsHeaders.find((c) => c.id === fieldId) ?? {}
- : {};
-
- if (column != null) {
- dispatch(
- upsertColumn({
- column: {
- category: column.category,
- columnHeaderType: 'not-filtered',
- description: isString(column.description) ? column.description : undefined,
- example: isString(column.example) ? column.example : undefined,
- id: fieldId,
- linkField: linkFields[fieldId] ?? undefined,
- type: column.type,
- aggregatable: column.aggregatable,
- initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
- ...initColumnHeader,
- },
- id: timelineId,
- index: result.destination != null ? result.destination.index : 0,
- })
- );
- } else {
- // create a column definition, because it doesn't exist in the browserFields:
- dispatch(
- upsertColumn({
- column: {
- columnHeaderType: 'not-filtered',
- id: fieldId,
- initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
- },
- id: timelineId,
- index: result.destination != null ? result.destination.index : 0,
- })
- );
- }
-};
-
-/**
- * Prevents fields from being dragged or dropped to any area other than column
- * header drop zone in the timeline
- */
-export const DRAG_TYPE_FIELD = 'drag-type-field';
-
-/** This class is added to the document body while dragging */
-export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
-
-/** This class is added to the document body while timeline field dragging */
-export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging';
-
export const allowTopN = ({
browserField,
fieldName,
@@ -347,125 +183,3 @@ export const allowTopN = ({
return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType);
};
-
-export const getTimelineIdFromColumnDroppableId = (droppableId: string) =>
- droppableId.slice(droppableId.lastIndexOf('.') + 1);
-
-/** The draggable will move this many pixes via the keyboard when the arrow key is pressed */
-export const KEYBOARD_DRAG_OFFSET = 20;
-
-export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper';
-
-/**
- * Temporarily disables tab focus on child links of the draggable to work
- * around an issue where tab focus becomes stuck on the interactive children
- *
- * NOTE: This function is (intentionally) only effective when used in a key
- * event handler, because it automatically restores focus capabilities on
- * the next tick.
- */
-export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => {
- const interactiveChildren = draggableElement.querySelectorAll('a, button');
- interactiveChildren.forEach((interactiveChild) => {
- interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation
- });
-
- // restore the default tabindexs on the next tick:
- setTimeout(() => {
- interactiveChildren.forEach((interactiveChild) => {
- interactiveChild.setAttribute('tabindex', '0'); // DOM mutation
- });
- }, 0);
-};
-
-export const draggableKeyDownHandler = ({
- beginDrag,
- cancelDragActions,
- closePopover,
- draggableElement,
- dragActions,
- dragToLocation,
- endDrag,
- keyboardEvent,
- openPopover,
- setDragActions,
-}: {
- beginDrag: () => FluidDragActions | null;
- cancelDragActions: () => void;
- closePopover?: () => void;
- draggableElement: HTMLDivElement;
- dragActions: FluidDragActions | null;
- dragToLocation: ({
- // eslint-disable-next-line @typescript-eslint/no-shadow
- dragActions,
- position,
- }: {
- dragActions: FluidDragActions | null;
- position: Position;
- }) => void;
- keyboardEvent: React.KeyboardEvent;
- endDrag: (dragActions: FluidDragActions | null) => void;
- openPopover?: () => void;
- setDragActions: (value: React.SetStateAction) => void;
-}) => {
- let currentPosition: DOMRect | null = null;
-
- switch (keyboardEvent.key) {
- case ' ':
- if (!dragActions) {
- // start dragging, because space was pressed
- if (closePopover != null) {
- closePopover();
- }
- setDragActions(beginDrag());
- } else {
- // end dragging, because space was pressed
- endDrag(dragActions);
- setDragActions(null);
- }
- break;
- case 'Escape':
- cancelDragActions();
- break;
- case 'Tab':
- // IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed
- temporarilyDisableInteractiveChildTabIndexes(draggableElement);
- break;
- case 'ArrowUp':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET },
- });
- break;
- case 'ArrowDown':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET },
- });
- break;
- case 'ArrowLeft':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
- });
- break;
- case 'ArrowRight':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
- });
- break;
- case 'Enter':
- stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER
- if (!dragActions && openPopover != null) {
- openPopover();
- }
- break;
- default:
- break;
- }
-};
diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx
index 9c6b8c485986e..f77bf0f347f79 100644
--- a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx
@@ -21,6 +21,8 @@ import {
tooltipContentIsExplicitlyNull,
} from '.';
+jest.mock('../../lib/kibana');
+
describe('draggables', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx
index a66d1d05025cb..2998b96fcf6ee 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx
@@ -11,10 +11,10 @@ import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
- EuiSpacer,
+ EuiForm,
+ EuiFormRow,
EuiText,
EuiTextArea,
- EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { CANCEL, COMMENT, COMMENT_PLACEHOLDER, CONFIRM } from './translations';
@@ -41,56 +41,62 @@ export const EndpointIsolateForm = memo(
);
return (
- <>
-
-
- {hostName} }}
- />{' '}
- {messageAppend}
-
-
+
+
+
+
+ {hostName} }}
+ />
+
+
+
+ {' '}
+ {messageAppend}
+
+
+
-
+
+
+
-
- {COMMENT}
-
-
-
-
-
-
-
-
- {CANCEL}
-
-
-
-
- {CONFIRM}
-
-
-
- >
+
+
+
+
+ {CANCEL}
+
+
+
+
+ {CONFIRM}
+
+
+
+
+
);
}
);
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
index b8f29996d603b..c782804b0592b 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
@@ -17,6 +17,8 @@ import { TestProviders } from '../../mock';
import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => {
return {
useRuleAsync: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
index 204b8c088304b..1be05cc560552 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
@@ -21,9 +21,8 @@ import { get, isEmpty } from 'lodash';
import memoizeOne from 'memoize-one';
import React from 'react';
import styled from 'styled-components';
-import { onFocusReFocusDraggable } from '../accessibility/helpers';
+import { onFocusReFocusDraggable } from '../../../../../timelines/public';
import { BrowserFields } from '../../containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { DragEffects } from '../drag_and_drop/draggable_wrapper';
import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper';
import { DRAG_TYPE_FIELD, getDroppableId } from '../drag_and_drop/helpers';
@@ -38,6 +37,7 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events';
import { getIconFromType, getExampleText } from './helpers';
import * as i18n from './translations';
import { EventFieldsData } from './types';
+import { ColumnHeaderOptions } from '../../../../common';
const HoverActionsContainer = styled(EuiPanel)`
align-items: center;
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
index 0c7515fe75d86..6aff259d8220e 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
@@ -20,6 +20,8 @@ import { mockAlertDetailsData } from './__mocks__';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { TimelineTabs } from '../../../../common/types/timeline';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../link_to');
describe('EventDetails', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
index f0865e1b8e083..555b67da953d6 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
@@ -16,6 +16,8 @@ import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
import { TimelineTabs } from '../../../../common/types/timeline';
+jest.mock('../../lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
index 93d0e6ccfbe3c..3ad7e9aef19dc 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
@@ -11,26 +11,24 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { rgba } from 'polished';
import styled from 'styled-components';
-
import {
arrayIndexToAriaIndex,
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
isTab,
onKeyDownFocusHandler,
-} from '../accessibility/helpers';
+} from '../../../../../timelines/public';
+
import { ADD_TIMELINE_BUTTON_CLASS_NAME } from '../../../timelines/components/flyout/add_timeline_button';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { BrowserFields, getAllFieldsByName } from '../../containers/source';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
-
import { getColumns } from './columns';
import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
-import { TimelineTabs } from '../../../../common/types/timeline';
+import { ColumnHeaderOptions, TimelineTabs } from '../../../../common/types/timeline';
interface Props {
browserFields: BrowserFields;
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
index 1f12c2de5e24f..8392be420a2c5 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
@@ -15,15 +15,15 @@ import {
getTableSkipFocus,
handleSkipFocus,
stopPropagationAndPreventDefault,
-} from '../accessibility/helpers';
+} from '../../../../../timelines/public';
import { BrowserField, BrowserFields } from '../../containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import {
DEFAULT_DATE_COLUMN_MIN_WIDTH,
DEFAULT_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
import * as i18n from './translations';
+import { ColumnHeaderOptions } from '../../../../common';
/**
* Defines the behavior of the search input that appears above the table of data
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx
index 7c84a325cb667..5051b39fe6093 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../common';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
index 36986f5f8d353..90a4e67d76b99 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
@@ -21,9 +21,8 @@ import { mockBrowserFields, mockDocValueFields } from '../../containers/source/m
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';
import { inputsModel } from '../../store/inputs';
-import { TimelineId } from '../../../../common/types/timeline';
+import { TimelineId, SortDirection } from '../../../../common/types/timeline';
import { KqlMode } from '../../../timelines/store/timeline/model';
-import { SortDirection } from '../../../timelines/components/timeline/body/sort';
import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
@@ -31,6 +30,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
import { useTimelineEvents } from '../../../timelines/containers';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
+jest.mock('../../lib/kibana');
+
jest.mock('../../hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
@@ -144,18 +145,18 @@ describe('EventsViewer', () => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]);
});
- test('call the right reduce action to show event details', async () => {
+ test('call the right reduce action to show event details', () => {
const wrapper = mount(
);
- await act(async () => {
+ act(() => {
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
});
- await waitFor(() => {
+ waitFor(() => {
expect(mockDispatch).toBeCalledTimes(2);
expect(mockDispatch.mock.calls[1][0]).toEqual({
payload: {
@@ -197,7 +198,7 @@ describe('EventsViewer', () => {
);
expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true);
});
- // TO DO sourcerer @X
+
test('it renders the footer containing the pagination', () => {
const wrapper = mount(
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
index c99275ec49ab3..5dadd740ae3bc 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
@@ -10,11 +10,12 @@ import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
+import { useDispatch } from 'react-redux';
import { Direction } from '../../../../common/search_strategy';
import { BrowserFields, DocValueFields } from '../../containers/source';
import { useTimelineEvents } from '../../../timelines/containers';
import { useKibana } from '../../lib/kibana';
-import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model';
+import { KqlMode } from '../../../timelines/store/timeline/model';
import { HeaderSection } from '../header_section';
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { Sort } from '../../../timelines/components/timeline/body/sort';
@@ -36,18 +37,21 @@ import {
Query,
} from '../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../store';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { ExitFullScreen } from '../exit_full_screen';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
-import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
-import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
+import {
+ ColumnHeaderOptions,
+ ControlColumnProps,
+ RowRenderer,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../common/types/timeline';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
-import {
- defaultControlColumn,
- ControlColumnProps,
-} from '../../../timelines/components/timeline/body/control_columns';
+import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
+import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
+import { useDeepEqualSelector } from '../../hooks/use_selector';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const UTILITY_BAR_HEIGHT = 19; // px
@@ -162,21 +166,19 @@ const EventsViewerComponent: React.FC = ({
utilityBar,
graphEventId,
}) => {
+ const dispatch = useDispatch();
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const kibana = useKibana();
const [isQueryLoading, setIsQueryLoading] = useState(false);
- const { getManageTimelineById, setIsTimelineLoading } = useManageTimeline();
-
useEffect(() => {
- setIsTimelineLoading({ id, isLoading: isQueryLoading });
- }, [id, isQueryLoading, setIsTimelineLoading]);
+ dispatch(timelineActions.updateIsLoading({ id, isLoading: isQueryLoading }));
+ }, [dispatch, id, isQueryLoading]);
- const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [
- getManageTimelineById,
- id,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const unit = useMemo(() => (n: number) => i18n.UNIT(n), []);
+ const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const justTitle = useMemo(() => {title} , [title]);
@@ -284,6 +286,7 @@ const EventsViewerComponent: React.FC = ({
{canQueryTimeline ? (
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
index cd27177643b44..571e04a106cf0 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
@@ -22,6 +22,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
import { useTimelineEvents } from '../../../timelines/containers';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../../../timelines/containers', () => ({
useTimelineEvents: jest.fn(),
}));
@@ -60,7 +62,9 @@ describe('StatefulEventsViewer', () => {
await waitFor(() => {
wrapper.update();
- expect(wrapper.find('[data-test-subj="events-viewer-panel"]').first().exists()).toBe(true);
+ expect(wrapper.text()).toMatchInlineSnapshot(
+ `"Showing: 12 events1 fields sorted@timestamp1event.severityevent.categoryevent.actionhost.namesource.ipdestination.ipdestination.bytesuser.name_idmessage0 of 12 events123"`
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
index b58aa2236d292..32aa716d4bce3 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
@@ -12,23 +12,23 @@ import styled from 'styled-components';
import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
-import { TimelineId } from '../../../../common/types/timeline';
+import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
import { Filter } from '../../../../../../../src/plugins/data/public';
-import { EventsViewer } from './events_viewer';
import { InspectButtonContainer } from '../inspect';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
+import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { DetailsPanel } from '../../../timelines/components/side_panel';
-import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
-
-const DEFAULT_EVENTS_VIEWER_HEIGHT = 652;
+import { useKibana } from '../../lib/kibana';
+import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
+import { EventsViewer } from './events_viewer';
const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
- height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : `${DEFAULT_EVENTS_VIEWER_HEIGHT}px`)};
+ height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)};
flex: 1 1 auto;
display: flex;
width: 100%;
@@ -83,6 +83,7 @@ const StatefulEventsViewerComponent: React.FC = ({
// If truthy, the graph viewer (Resolver) is showing
graphEventId,
}) => {
+ const { timelines: timelinesUi } = useKibana().services;
const {
browserFields,
docValueFields,
@@ -90,8 +91,9 @@ const StatefulEventsViewerComponent: React.FC = ({
selectedPatterns,
loading: isLoadingIndexPattern,
} = useSourcererScope(scopeId);
- const { globalFullScreen } = useGlobalFullScreen();
-
+ const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
+ // TODO: Once we are past experimental phase this code should be removed
+ const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
useEffect(() => {
if (createTimeline != null) {
createTimeline({
@@ -111,37 +113,73 @@ const StatefulEventsViewerComponent: React.FC = ({
}, []);
const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
+ const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
+ const trailingControlColumns: ControlColumnProps[] = [];
return (
<>
-
+ {tGridEnabled ? (
+ timelinesUi.getTGrid<'embedded'>({
+ type: 'embedded',
+ browserFields,
+ columns,
+ dataProviders: dataProviders!,
+ deletedEventIds,
+ docValueFields,
+ end,
+ filters: globalFilters,
+ globalFullScreen,
+ headerFilterGroup,
+ id,
+ indexNames: selectedPatterns,
+ indexPattern,
+ isLive,
+ isLoadingIndexPattern,
+ itemsPerPage,
+ itemsPerPageOptions: itemsPerPageOptions!,
+ kqlMode,
+ query,
+ onRuleChange,
+ renderCellValue,
+ rowRenderers,
+ setGlobalFullScreen,
+ start,
+ sort,
+ utilityBar,
+ graphEventId,
+ leadingControlColumns,
+ trailingControlColumns,
+ })
+ ) : (
+
+ )}
i18n.translate('xpack.securitySolution.eventsViewer.unit', {
values: { totalCount },
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx
index c13a1b011ccbd..87f7f5fe2f507 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx
@@ -106,7 +106,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({
diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap
index 994e98d8619a1..51326d54a6161 100644
--- a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap
@@ -4,17 +4,19 @@ exports[`rendering renders correctly 1`] = `
}
>
-
-
Additional filters here.
-
-
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx
index c6b5b6ccde5cd..79c08e50451f7 100644
--- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx
@@ -8,18 +8,9 @@
import React from 'react';
import styled from 'styled-components';
import { InPortal } from 'react-reverse-portal';
-
+import { EuiPanel } from '@elastic/eui';
import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal';
-const Wrapper = styled.aside`
- position: relative;
- z-index: ${({ theme }) => theme.eui.euiZNavigation};
- background: ${({ theme }) => theme.eui.euiColorEmptyShade};
- border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
- padding: ${({ theme }) => theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l};
-`;
-Wrapper.displayName = 'Wrapper';
-
const FiltersGlobalContainer = styled.header<{ show: boolean }>`
display: ${({ show }) => (show ? 'block' : 'none')};
`;
@@ -32,13 +23,15 @@ export interface FiltersGlobalProps {
}
export const FiltersGlobal = React.memo(({ children, show = true }) => {
- const { globalHeaderPortalNode } = useGlobalHeaderPortal();
+ const { globalKQLHeaderPortalNode } = useGlobalHeaderPortal();
return (
-
-
- {children}
-
+
+
+
+ {children}
+
+
);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx
deleted file mode 100644
index 96a7eacb7fb08..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { mount } from 'enzyme';
-
-import { useGetUserCasesPermissions } from '../../../common/lib/kibana';
-import { TestProviders } from '../../../common/mock';
-import { HeaderGlobal } from '.';
-
-jest.mock('../../../common/lib/kibana');
-
-describe('HeaderGlobal', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('does not display the cases tab when the user does not have read permissions', () => {
- (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
- crud: false,
- read: false,
- });
-
- const wrapper = mount(
-
-
-
- );
-
- expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeFalsy();
- });
-
- it('displays the cases tab when the user has read permissions', () => {
- (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
- crud: true,
- read: true,
- });
-
- const wrapper = mount(
-
-
-
- );
-
- expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeTruthy();
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
deleted file mode 100644
index e91905183aab1..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
-import { pickBy } from 'lodash/fp';
-import React, { forwardRef, useCallback, useMemo } from 'react';
-import styled from 'styled-components';
-import { OutPortal } from 'react-reverse-portal';
-
-import { navTabs } from '../../../app/home/home_navigations';
-import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen';
-import { SecurityPageName } from '../../../app/types';
-import { getAppOverviewUrl } from '../link_to';
-import { MlPopover } from '../ml_popover/ml_popover';
-import { SiemNavigation } from '../navigation';
-import * as i18n from './translations';
-import { useGetUrlSearch } from '../navigation/use_get_url_search';
-import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana';
-import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
-import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal';
-import { LinkAnchor } from '../links';
-
-const Wrapper = styled.header<{ $isFixed: boolean }>`
- ${({ theme, $isFixed }) => `
- background: ${theme.eui.euiColorEmptyShade};
- border-bottom: ${theme.eui.euiBorderThin};
- width: 100%;
- z-index: ${theme.eui.euiZNavigation};
- position: ${$isFixed ? 'fixed' : 'relative'};
- `}
-`;
-Wrapper.displayName = 'Wrapper';
-
-const WrapperContent = styled.div<{ $globalFullScreen: boolean }>`
- display: ${({ $globalFullScreen }) => ($globalFullScreen ? 'none' : 'block')};
- padding-top: ${({ $globalFullScreen, theme }) =>
- $globalFullScreen ? theme.eui.paddingSizes.s : theme.eui.paddingSizes.m};
-`;
-
-WrapperContent.displayName = 'WrapperContent';
-
-const FlexItem = styled(EuiFlexItem)`
- min-width: 0;
-`;
-FlexItem.displayName = 'FlexItem';
-
-const FlexGroup = styled(EuiFlexGroup)<{ $hasSibling: boolean }>`
- ${({ $hasSibling, theme }) => `
- border-bottom: ${theme.eui.euiBorderThin};
- margin-bottom: 1px;
- padding-bottom: 4px;
- padding-left: ${theme.eui.paddingSizes.l};
- padding-right: ${theme.eui.paddingSizes.l};
- ${$hasSibling ? `border-bottom: ${theme.eui.euiBorderThin};` : 'border-bottom-width: 0px;'}
- `}
-`;
-FlexGroup.displayName = 'FlexGroup';
-
-interface HeaderGlobalProps {
- hideDetectionEngine?: boolean;
- isFixed?: boolean;
-}
-
-export const HeaderGlobal = React.memo(
- forwardRef(
- ({ hideDetectionEngine = false, isFixed = true }, ref) => {
- const { globalHeaderPortalNode } = useGlobalHeaderPortal();
- const { globalFullScreen } = useGlobalFullScreen();
- const { timelineFullScreen } = useTimelineFullScreen();
- const search = useGetUrlSearch(navTabs.overview);
- const { application, http } = useKibana().services;
- const { navigateToApp, getUrlForApp } = application;
- const overviewPath = useMemo(
- () => getUrlForApp(APP_ID, { path: SecurityPageName.overview }),
- [getUrlForApp]
- );
- const overviewHref = useMemo(() => getAppOverviewUrl(overviewPath, search), [
- overviewPath,
- search,
- ]);
-
- const basePath = http.basePath.get();
- const goToOverview = useCallback(
- (ev) => {
- ev.preventDefault();
- navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { path: search });
- },
- [navigateToApp, search]
- );
-
- const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
-
- // build a list of tabs to exclude
- const tabsToExclude = new Set([
- ...(hideDetectionEngine ? [SecurityPageName.detections] : []),
- ...(!hasCasesReadPermissions ? [SecurityPageName.case] : []),
- ]);
-
- // include the tab if it is not in the set of excluded ones
- const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {window.location.pathname.includes(APP_DETECTIONS_PATH) && (
-
-
-
- )}
-
-
-
- {i18n.BUTTON_ADD_DATA}
-
-
-
-
-
-
-
-
- );
- }
- )
-);
-HeaderGlobal.displayName = 'HeaderGlobal';
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
index 84c8971e3d352..9cb9f28612b15 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
@@ -1,14 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeaderPage it renders 1`] = `
-
-
+
-
+
-
-
+
Test supplement
-
-
-
+
+
+
-
+
`;
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx
index 78bac02585b9f..8a1748de582c4 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx
@@ -57,7 +57,7 @@ describe('HeaderPage', () => {
);
- expect(wrapper.find('.siemHeaderPage__linkBack').first().exists()).toBe(true);
+ expect(wrapper.find('.securitySolutionHeaderPage__linkBack').first().exists()).toBe(true);
});
test('it DOES NOT render the back link when not provided', () => {
@@ -67,7 +67,7 @@ describe('HeaderPage', () => {
);
- expect(wrapper.find('.siemHeaderPage__linkBack').first().exists()).toBe(false);
+ expect(wrapper.find('.securitySolutionHeaderPage__linkBack').first().exists()).toBe(false);
});
test('it renders the first subtitle when provided', () => {
@@ -134,27 +134,21 @@ describe('HeaderPage', () => {
expect(wrapper.find('[data-test-subj="header-page-supplements"]').first().exists()).toBe(false);
});
- test('it applies border styles when border is true', () => {
- const wrapper = mount(
-
-
-
- );
- const siemHeaderPage = wrapper.find('.siemHeaderPage').first();
-
- expect(siemHeaderPage).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
- expect(siemHeaderPage).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
- });
-
test('it DOES NOT apply border styles when border is false', () => {
const wrapper = mount(
);
- const siemHeaderPage = wrapper.find('.siemHeaderPage').first();
+ const securitySolutionHeaderPage = wrapper.find('.securitySolutionHeaderPage').first();
- expect(siemHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
- expect(siemHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
+ expect(securitySolutionHeaderPage).not.toHaveStyleRule(
+ 'border-bottom',
+ euiDarkVars.euiBorderThin
+ );
+ expect(securitySolutionHeaderPage).not.toHaveStyleRule(
+ 'padding-bottom',
+ euiDarkVars.paddingSizes.l
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
index d01869bb6999b..1c87d70c0c7cb 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
@@ -5,7 +5,13 @@
* 2.0.
*/
-import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui';
+import {
+ EuiBadge,
+ EuiProgress,
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiSpacer,
+} from '@elastic/eui';
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import styled, { css } from 'styled-components';
@@ -25,36 +31,16 @@ interface HeaderProps {
}
const Header = styled.header.attrs({
- className: 'siemHeaderPage',
+ className: 'securitySolutionHeaderPage',
})`
${({ border, theme }) => css`
margin-bottom: ${theme.eui.euiSizeL};
-
- ${border &&
- css`
- border-bottom: ${theme.eui.euiBorderThin};
- padding-bottom: ${theme.eui.paddingSizes.l};
- .euiProgress {
- top: ${theme.eui.paddingSizes.l};
- }
- `}
`}
`;
Header.displayName = 'Header';
-const FlexItem = styled(EuiFlexItem)`
- ${({ theme }) => css`
- display: block;
-
- @media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) {
- max-width: 50%;
- }
- `}
-`;
-FlexItem.displayName = 'FlexItem';
-
const LinkBack = styled.div.attrs({
- className: 'siemHeaderPage__linkBack',
+ className: 'securitySolutionHeaderPage__linkBack',
})`
${({ theme }) => css`
font-size: ${theme.eui.euiFontSizeXS};
@@ -117,9 +103,9 @@ const HeaderPageComponent: React.FC = ({
[backOptions, history]
);
return (
-
-
-
+ <>
+
+
{backOptions && (
= ({
{subtitle && }
{subtitle2 && }
{border && isLoading && }
-
+
{children && (
-
+
{children}
-
+
)}
-
- {!hideSourcerer && }
-
+ {!hideSourcerer && }
+
+ {/* Manually add a 'padding-bottom' to header */}
+
+ >
);
};
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx
index 7ad9de29431c9..d21adbd00cc20 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../mock';
import { Title } from './title';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
describe('Title', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx
index ddbcf710aff30..a0e2ff266ad28 100644
--- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx
@@ -131,7 +131,7 @@ const InspectButtonComponent: React.FC = ({
color="text"
iconSide="left"
iconType="inspect"
- isDisabled={loading || isDisabled}
+ isDisabled={loading || isDisabled || false}
isLoading={loading}
onClick={handleClick}
>
@@ -145,7 +145,7 @@ const InspectButtonComponent: React.FC = ({
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
- isDisabled={loading || isDisabled}
+ isDisabled={loading || isDisabled || false}
title={i18n.INSPECT}
onClick={handleClick}
/>
diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap
index c7841f6d6bbcc..f0fd8427140df 100644
--- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap
@@ -14,6 +14,7 @@ exports[`item_details_card ItemDetailsAction should render correctly 1`] = `
exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = `
(
);
return (
-
+
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
index 115fb65dc7011..f08edb114b9a9 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
@@ -13,6 +13,8 @@ import { EntityDraggableComponent } from './entity_draggable';
import { TestProviders } from '../../mock/test_providers';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
index 6ad2bd30283d2..0d9b4001c17aa 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
@@ -17,6 +17,8 @@ import { useMountAppended } from '../../../utils/use_mount_appended';
import { Anomalies } from '../types';
import { waitFor } from '@testing-library/dom';
+jest.mock('../../../lib/kibana');
+
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
index 6b569a67cfebf..5eb0751404872 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
@@ -18,6 +18,8 @@ import { Anomalies } from '../types';
import { useMountAppended } from '../../../utils/use_mount_appended';
import { waitFor } from '@testing-library/dom';
+jest.mock('../../../lib/kibana');
+
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';
const narrowDateRange = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx
index ae6ef4e680ffa..2ecda8482e340 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx
@@ -16,6 +16,8 @@ import { Columns } from '../../paginated_table';
import { TestProviders } from '../../../mock';
import { useMountAppended } from '../../../utils/use_mount_appended';
+jest.mock('../../../lib/kibana');
+
const startDate = new Date(2001).toISOString();
const endDate = new Date(3000).toISOString();
const interval = 'days';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx
index b8a8ab88a74fd..48c2ec3ee38d8 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx
@@ -15,6 +15,8 @@ import React from 'react';
import { TestProviders } from '../../../mock';
import { useMountAppended } from '../../../utils/use_mount_appended';
+jest.mock('../../../../common/lib/kibana');
+
const startDate = new Date(2001).toISOString();
const endDate = new Date(3000).toISOString();
describe('get_anomalies_network_table_columns', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
index 561805217e8a1..cc6ac5355f90b 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
@@ -5,7 +5,13 @@
* 2.0.
*/
-import { EuiButtonEmpty, EuiCallOut, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui';
+import {
+ EuiHeaderSectionItemButton,
+ EuiCallOut,
+ EuiPopover,
+ EuiPopoverTitle,
+ EuiSpacer,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { Dispatch, useCallback, useReducer, useState } from 'react';
@@ -115,14 +121,19 @@ export const MlPopover = React.memo(() => {
anchorPosition="downRight"
id="integrations-popover"
button={
- setIsPopoverOpen(!isPopoverOpen)}
+ textProps={{ style: { fontSize: '1rem' } }}
>
{i18n.ML_JOB_SETTINGS}
-
+
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}
@@ -138,7 +149,11 @@ export const MlPopover = React.memo(() => {
anchorPosition="downRight"
id="integrations-popover"
button={
- {
setIsPopoverOpen(!isPopoverOpen);
dispatch({ type: 'refresh' });
}}
+ textProps={{ style: { fontSize: '1rem' } }}
>
{i18n.ML_JOB_SETTINGS}
-
+
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
index dffc7becaf42a..c869df6ad388e 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
@@ -306,6 +306,29 @@ describe('Navigation Breadcrumbs', () => {
},
]);
});
+
+ test('should set "timeline.isOpen" to false when timeline is open', () => {
+ const breadcrumbs = getBreadcrumbsForRoute(
+ {
+ ...getMockObject('timelines', '/', undefined),
+ timeline: {
+ activeTab: TimelineTabs.query,
+ id: 'TIMELINE_ID',
+ isOpen: true,
+ graphEventId: 'GRAPH_EVENT_ID',
+ },
+ },
+ getUrlForAppMock
+ );
+ expect(breadcrumbs).toEqual([
+ { text: 'Security', href: 'securitySolutionoverview' },
+ {
+ text: 'Timelines',
+ href:
+ "securitySolution:timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(activeTab:query,graphEventId:GRAPH_EVENT_ID,id:TIMELINE_ID,isOpen:!f)",
+ },
+ ]);
+ });
});
describe('setBreadcrumbs()', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
index 605478900d066..a09945f705c58 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
@@ -61,10 +61,14 @@ const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute
// eslint-disable-next-line complexity
export const getBreadcrumbsForRoute = (
- object: RouteSpyState & TabNavigationProps,
+ objectParam: RouteSpyState & TabNavigationProps,
getUrlForApp: GetUrlForApp
): ChromeBreadcrumb[] | null => {
- const spyState: RouteSpyState = omit('navTabs', object);
+ const spyState: RouteSpyState = omit('navTabs', objectParam);
+
+ // Sets `timeline.isOpen` to false in the state to avoid reopening the timeline on breadcrumb click. https://github.com/elastic/kibana/issues/100322
+ const object = { ...objectParam, timeline: { ...objectParam.timeline, isOpen: false } };
+
const overviewPath = getUrlForApp(APP_ID, { path: SecurityPageName.overview });
const siemRootBreadcrumb: ChromeBreadcrumb = {
text: APP_NAME,
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
index 27db326dddec5..c75b38e03acb4 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
@@ -9,12 +9,12 @@ import { mount } from 'enzyme';
import React from 'react';
import { CONSTANTS } from '../url_state/constants';
-import { SiemNavigationComponent } from './';
+import { TabNavigationComponent } from './';
import { setBreadcrumbs } from './breadcrumbs';
import { navTabs } from '../../../app/home/home_navigations';
import { HostsTableType } from '../../../hosts/store/model';
import { RouteSpyState } from '../../utils/route/types';
-import { SiemNavigationProps, SiemNavigationComponentProps } from './types';
+import { TabNavigationComponentProps, SecuritySolutionTabNavigationProps } from './types';
import { TimelineTabs } from '../../../../common/types/timeline';
jest.mock('react-router-dom', () => {
@@ -48,7 +48,9 @@ jest.mock('../../lib/kibana', () => {
jest.mock('../link_to');
describe('SIEM Navigation', () => {
- const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = {
+ const mockProps: TabNavigationComponentProps &
+ SecuritySolutionTabNavigationProps &
+ RouteSpyState = {
pageName: 'hosts',
pathName: '/',
detailName: undefined,
@@ -89,7 +91,7 @@ describe('SIEM Navigation', () => {
},
},
};
- const wrapper = mount( );
+ const wrapper = mount( );
test('it calls setBreadcrumbs with correct path on mount', () => {
expect(setBreadcrumbs).toHaveBeenNthCalledWith(
1,
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx
index 7ea0b26ae8b3b..233b4b2cb1d02 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx
@@ -16,75 +16,93 @@ import { useRouteSpy } from '../../utils/route/use_route_spy';
import { makeMapStateToProps } from '../url_state/helpers';
import { setBreadcrumbs } from './breadcrumbs';
import { TabNavigation } from './tab_navigation';
-import { SiemNavigationProps, SiemNavigationComponentProps } from './types';
+import { TabNavigationComponentProps, SecuritySolutionTabNavigationProps } from './types';
-export const SiemNavigationComponent: React.FC<
- SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState
-> = ({
- detailName,
- display,
- navTabs,
- pageName,
- pathName,
- search,
- tabName,
- urlState,
- flowTarget,
- state,
-}) => {
- const {
- chrome,
- application: { getUrlForApp },
- } = useKibana().services;
+/**
+ * @description - This component handels all of the tab navigation seen within a Security Soluton application page, not the Security Solution primary side navigation
+ * For the primary side nav see './use_security_solution_navigation'
+ */
+export const TabNavigationComponent: React.FC<
+ RouteSpyState & SecuritySolutionTabNavigationProps & TabNavigationComponentProps
+> = React.memo(
+ ({
+ detailName,
+ display,
+ flowTarget,
+ navTabs,
+ pageName,
+ pathName,
+ search,
+ state,
+ tabName,
+ urlState,
+ }) => {
+ const {
+ chrome,
+ application: { getUrlForApp },
+ } = useKibana().services;
- useEffect(() => {
- if (pathName || pageName) {
- setBreadcrumbs(
- {
- detailName,
- filters: urlState.filters,
- flowTarget,
- navTabs,
- pageName,
- pathName,
- query: urlState.query,
- savedQuery: urlState.savedQuery,
- search,
- sourcerer: urlState.sourcerer,
- state,
- tabName,
- timeline: urlState.timeline,
- timerange: urlState.timerange,
- },
- chrome,
- getUrlForApp
- );
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [chrome, pageName, pathName, search, navTabs, urlState, state]);
+ useEffect(() => {
+ if (pathName || pageName) {
+ setBreadcrumbs(
+ {
+ detailName,
+ filters: urlState.filters,
+ flowTarget,
+ navTabs,
+ pageName,
+ pathName,
+ query: urlState.query,
+ savedQuery: urlState.savedQuery,
+ search,
+ sourcerer: urlState.sourcerer,
+ state,
+ tabName,
+ timeline: urlState.timeline,
+ timerange: urlState.timerange,
+ },
+ chrome,
+ getUrlForApp
+ );
+ }
+ }, [
+ chrome,
+ pageName,
+ pathName,
+ search,
+ navTabs,
+ urlState,
+ state,
+ detailName,
+ flowTarget,
+ tabName,
+ getUrlForApp,
+ ]);
- return (
-
- );
-};
+ return (
+
+ );
+ }
+);
+TabNavigationComponent.displayName = 'TabNavigationComponent';
-export const SiemNavigationRedux = compose<
- React.ComponentClass
+export const SecuritySolutionTabNavigationRedux = compose<
+ React.ComponentClass
>(connect(makeMapStateToProps))(
React.memo(
- SiemNavigationComponent,
+ TabNavigationComponent,
(prevProps, nextProps) =>
prevProps.pathName === nextProps.pathName &&
prevProps.search === nextProps.search &&
@@ -94,16 +112,16 @@ export const SiemNavigationRedux = compose<
)
);
-const SiemNavigationContainer: React.FC = (props) => {
- const [routeProps] = useRouteSpy();
- const stateNavReduxProps: RouteSpyState & SiemNavigationProps = {
- ...routeProps,
- ...props,
- };
-
- return ;
-};
+export const SecuritySolutionTabNavigation: React.FC = React.memo(
+ (props) => {
+ const [routeProps] = useRouteSpy();
+ const stateNavReduxProps: RouteSpyState & SecuritySolutionTabNavigationProps = {
+ ...routeProps,
+ ...props,
+ };
-export const SiemNavigation = React.memo(SiemNavigationContainer, (prevProps, nextProps) =>
- deepEqual(prevProps.navTabs, nextProps.navTabs)
+ return ;
+ },
+ (prevProps, nextProps) => deepEqual(prevProps.navTabs, nextProps.navTabs)
);
+SecuritySolutionTabNavigation.displayName = 'SecuritySolutionTabNavigation';
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts
index 4253d08d1ed19..53565d79e6948 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts
@@ -7,17 +7,17 @@
import { UrlInputsModel } from '../../../store/inputs/model';
import { CONSTANTS } from '../../url_state/constants';
-import { HostsTableType } from '../../../../hosts/store/model';
import { SourcererScopePatterns } from '../../../store/sourcerer/model';
import { TimelineUrl } from '../../../../timelines/store/timeline/model';
import { Filter, Query } from '../../../../../../../../src/plugins/data/public';
-import { SiemNavigationProps } from '../types';
+import { SecuritySolutionTabNavigationProps } from '../types';
+import { SiemRouteType } from '../../../utils/route/types';
-export interface TabNavigationProps extends SiemNavigationProps {
+export interface TabNavigationProps extends SecuritySolutionTabNavigationProps {
pathName: string;
pageName: string;
- tabName: HostsTableType | undefined;
+ tabName: SiemRouteType | undefined;
[CONSTANTS.appQuery]?: Query;
[CONSTANTS.filters]?: Filter[];
[CONSTANTS.savedQuery]?: string;
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
index 9700afcb8cd59..1c317700b1d15 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
@@ -5,31 +5,20 @@
* 2.0.
*/
-import { Filter, Query } from '../../../../../../../src/plugins/data/public';
-import { HostsTableType } from '../../../hosts/store/model';
-import { UrlInputsModel } from '../../store/inputs/model';
-import { TimelineUrl } from '../../../timelines/store/timeline/model';
-import { CONSTANTS, UrlStateType } from '../url_state/constants';
+import { UrlStateType } from '../url_state/constants';
import { SecurityPageName } from '../../../app/types';
-import { SourcererScopePatterns } from '../../store/sourcerer/model';
+import { UrlState } from '../url_state/types';
+import { SiemRouteType } from '../../utils/route/types';
-export interface SiemNavigationProps {
+export interface SecuritySolutionTabNavigationProps {
display?: 'default' | 'condensed';
navTabs: Record;
}
-
-export interface SiemNavigationComponentProps {
- pathName: string;
+export interface TabNavigationComponentProps {
pageName: string;
- tabName: HostsTableType | undefined;
- urlState: {
- [CONSTANTS.appQuery]?: Query;
- [CONSTANTS.filters]?: Filter[];
- [CONSTANTS.savedQuery]?: string;
- [CONSTANTS.sourcerer]: SourcererScopePatterns;
- [CONSTANTS.timerange]: UrlInputsModel;
- [CONSTANTS.timeline]: TimelineUrl;
- };
+ tabName: SiemRouteType | undefined;
+ urlState: UrlState;
+ pathName: string;
}
export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean };
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
new file mode 100644
index 0000000000000..ef00bef841305
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
@@ -0,0 +1,214 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderHook } from '@testing-library/react-hooks';
+import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public';
+import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana';
+import { SecurityPageName } from '../../../../app/types';
+import { useSecuritySolutionNavigation } from '.';
+import { CONSTANTS } from '../../url_state/constants';
+import { TimelineTabs } from '../../../../../common/types/timeline';
+import { useDeepEqualSelector } from '../../../hooks/use_selector';
+import { UrlInputsModel } from '../../../store/inputs/model';
+import { useRouteSpy } from '../../../utils/route/use_route_spy';
+
+jest.mock('../../../lib/kibana');
+jest.mock('../../../hooks/use_selector');
+jest.mock('../../../utils/route/use_route_spy');
+
+describe('useSecuritySolutionNavigation', () => {
+ const mockUrlState = {
+ [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' },
+ [CONSTANTS.savedQuery]: '',
+ [CONSTANTS.sourcerer]: {},
+ [CONSTANTS.timeline]: {
+ activeTab: TimelineTabs.query,
+ id: '',
+ isOpen: false,
+ graphEventId: '',
+ },
+ [CONSTANTS.timerange]: {
+ global: {
+ [CONSTANTS.timerange]: {
+ from: '2020-07-07T08:20:18.966Z',
+ fromStr: 'now-24h',
+ kind: 'relative',
+ to: '2020-07-08T08:20:18.966Z',
+ toStr: 'now',
+ },
+ linkTo: ['timeline'],
+ },
+ timeline: {
+ [CONSTANTS.timerange]: {
+ from: '2020-07-07T08:20:18.966Z',
+ fromStr: 'now-24h',
+ kind: 'relative',
+ to: '2020-07-08T08:20:18.966Z',
+ toStr: 'now',
+ },
+ linkTo: ['global'],
+ },
+ } as UrlInputsModel,
+ };
+
+ const mockRouteSpy = [
+ {
+ detailName: '',
+ flowTarget: '',
+ pathName: '',
+ search: '',
+ state: '',
+ tabName: '',
+ pageName: SecurityPageName.hosts,
+ },
+ ];
+
+ beforeEach(() => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState });
+ (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy);
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ navigateToApp: jest.fn(),
+ getUrlForApp: (appId: string, options?: { path?: string; absolute?: boolean }) =>
+ `${appId}${options?.path ?? ''}`,
+ },
+ chrome: {
+ setBreadcrumbs: jest.fn(),
+ },
+ },
+ });
+ });
+
+ it('should create navigation config', async () => {
+ const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() =>
+ useSecuritySolutionNavigation()
+ );
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "icon": "logoSecurity",
+ "items": Array [
+ Object {
+ "id": "securitySolution",
+ "items": Array [
+ Object {
+ "data-href": "securitySolution:overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "data-test-subj": "navigation-overview",
+ "disabled": false,
+ "href": "securitySolution:overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "id": "overview",
+ "isSelected": false,
+ "name": "Overview",
+ "onClick": [Function],
+ },
+ Object {
+ "data-href": "securitySolution:detections?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "data-test-subj": "navigation-detections",
+ "disabled": false,
+ "href": "securitySolution:detections?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "id": "detections",
+ "isSelected": false,
+ "name": "Detections",
+ "onClick": [Function],
+ },
+ Object {
+ "data-href": "securitySolution:hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "data-test-subj": "navigation-hosts",
+ "disabled": false,
+ "href": "securitySolution:hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "id": "hosts",
+ "isSelected": true,
+ "name": "Hosts",
+ "onClick": [Function],
+ },
+ Object {
+ "data-href": "securitySolution:network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "data-test-subj": "navigation-network",
+ "disabled": false,
+ "href": "securitySolution:network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "id": "network",
+ "isSelected": false,
+ "name": "Network",
+ "onClick": [Function],
+ },
+ Object {
+ "data-href": "securitySolution:timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "data-test-subj": "navigation-timelines",
+ "disabled": false,
+ "href": "securitySolution:timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "id": "timelines",
+ "isSelected": false,
+ "name": "Timelines",
+ "onClick": [Function],
+ },
+ Object {
+ "data-href": "securitySolution:administration",
+ "data-test-subj": "navigation-administration",
+ "disabled": false,
+ "href": "securitySolution:administration",
+ "id": "administration",
+ "isSelected": false,
+ "name": "Administration",
+ "onClick": [Function],
+ },
+ ],
+ "name": "",
+ },
+ ],
+ "name": "Security",
+ }
+ `);
+ });
+
+ describe('Permission gated routes', () => {
+ describe('cases', () => {
+ it('should display the cases navigation item when the user has read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: true,
+ read: true,
+ });
+
+ const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() =>
+ useSecuritySolutionNavigation()
+ );
+
+ const caseNavItem = (result.current?.items || [])[0].items?.find(
+ (item) => item['data-test-subj'] === 'navigation-case'
+ );
+ expect(caseNavItem).toMatchInlineSnapshot(`
+ Object {
+ "data-href": "securitySolution:case?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "data-test-subj": "navigation-case",
+ "disabled": false,
+ "href": "securitySolution:case?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "id": "case",
+ "isSelected": false,
+ "name": "Cases",
+ "onClick": [Function],
+ }
+ `);
+ });
+
+ it('should not display the cases navigation item when the user does not have read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
+ const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() =>
+ useSecuritySolutionNavigation()
+ );
+
+ const caseNavItem = (result.current?.items || [])[0].items?.find(
+ (item) => item['data-test-subj'] === 'navigation-case'
+ );
+ expect(caseNavItem).toBeFalsy();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx
new file mode 100644
index 0000000000000..f2aee86912dd7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useEffect } from 'react';
+import { pickBy } from 'lodash/fp';
+import { usePrimaryNavigation } from './use_primary_navigation';
+import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana';
+import { setBreadcrumbs } from '../breadcrumbs';
+import { makeMapStateToProps } from '../../url_state/helpers';
+import { useRouteSpy } from '../../../utils/route/use_route_spy';
+import { navTabs } from '../../../../app/home/home_navigations';
+import { useDeepEqualSelector } from '../../../hooks/use_selector';
+import { SecurityPageName } from '../../../../../common/constants';
+
+/**
+ * @description - This hook provides the structure necessary by the KibanaPageTemplate for rendering the primary security_solution side navigation.
+ * TODO: Consolidate & re-use the logic in the hooks in this directory that are replicated from the tab_navigation to maintain breadcrumbs, telemetry, etc...
+ */
+export const useSecuritySolutionNavigation = () => {
+ const [routeProps] = useRouteSpy();
+ const urlMapState = makeMapStateToProps();
+ const { urlState } = useDeepEqualSelector(urlMapState);
+ const {
+ chrome,
+ application: { getUrlForApp },
+ } = useKibana().services;
+
+ const { detailName, flowTarget, pageName, pathName, search, state, tabName } = routeProps;
+
+ useEffect(() => {
+ if (pathName || pageName) {
+ setBreadcrumbs(
+ {
+ detailName,
+ filters: urlState.filters,
+ flowTarget,
+ navTabs,
+ pageName,
+ pathName,
+ query: urlState.query,
+ savedQuery: urlState.savedQuery,
+ search,
+ sourcerer: urlState.sourcerer,
+ state,
+ tabName,
+ timeline: urlState.timeline,
+ timerange: urlState.timerange,
+ },
+ chrome,
+ getUrlForApp
+ );
+ }
+ }, [
+ chrome,
+ pageName,
+ pathName,
+ search,
+ urlState,
+ state,
+ detailName,
+ flowTarget,
+ tabName,
+ getUrlForApp,
+ ]);
+
+ const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
+
+ // build a list of tabs to exclude
+ const tabsToExclude = new Set([
+ ...(!hasCasesReadPermissions ? [SecurityPageName.case] : []),
+ ]);
+
+ // include the tab if it is not in the set of excluded ones
+ const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs);
+
+ return usePrimaryNavigation({
+ query: urlState.query,
+ filters: urlState.filters,
+ navTabs: tabsToDisplay,
+ pageName,
+ sourcerer: urlState.sourcerer,
+ savedQuery: urlState.savedQuery,
+ timeline: urlState.timeline,
+ timerange: urlState.timerange,
+ });
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts
new file mode 100644
index 0000000000000..f639b8a37f0da
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TabNavigationProps } from '../tab_navigation/types';
+
+export type PrimaryNavigationItemsProps = Omit<
+ TabNavigationProps,
+ 'pathName' | 'pageName' | 'tabName'
+> & { selectedTabId: string };
+
+export type PrimaryNavigationProps = Omit;
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
new file mode 100644
index 0000000000000..42ca7f4c65460
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { APP_ID } from '../../../../../common/constants';
+import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry';
+import { getSearch } from '../helpers';
+import { PrimaryNavigationItemsProps } from './types';
+import { useKibana } from '../../../lib/kibana';
+
+export const usePrimaryNavigationItems = ({
+ filters,
+ navTabs,
+ query,
+ savedQuery,
+ selectedTabId,
+ sourcerer,
+ timeline,
+ timerange,
+}: PrimaryNavigationItemsProps) => {
+ const { navigateToApp, getUrlForApp } = useKibana().services.application;
+
+ const navItems = Object.values(navTabs).map((tab) => {
+ const { id, name, disabled } = tab;
+ const isSelected = selectedTabId === id;
+ const urlSearch = getSearch(tab, {
+ filters,
+ query,
+ savedQuery,
+ sourcerer,
+ timeline,
+ timerange,
+ });
+
+ const handleClick = (ev: React.MouseEvent) => {
+ ev.preventDefault();
+ navigateToApp(`${APP_ID}:${id}`, { path: urlSearch });
+ track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${id}`);
+ };
+
+ const appHref = getUrlForApp(`${APP_ID}:${id}`, { path: urlSearch });
+
+ return {
+ 'data-href': appHref,
+ 'data-test-subj': `navigation-${id}`,
+ disabled,
+ href: appHref,
+ id,
+ isSelected,
+ name,
+ onClick: handleClick,
+ };
+ });
+
+ return [
+ {
+ id: APP_ID, // TODO: When separating into sub-sections (detect, explore, investigate). Those names can also serve as the section id
+ items: navItems,
+ name: '',
+ },
+ ];
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx
new file mode 100644
index 0000000000000..390f44b48b0b1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getOr } from 'lodash/fp';
+import { useEffect, useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { PrimaryNavigationProps } from './types';
+import { usePrimaryNavigationItems } from './use_navigation_items';
+import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public';
+
+const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mainLabel', {
+ defaultMessage: 'Security',
+});
+
+export const usePrimaryNavigation = ({
+ filters,
+ query,
+ navTabs,
+ pageName,
+ savedQuery,
+ sourcerer,
+ timeline,
+ timerange,
+}: PrimaryNavigationProps): KibanaPageTemplateProps['solutionNav'] => {
+ const mapLocationToTab = useCallback(
+ (): string =>
+ getOr(
+ '',
+ 'id',
+ Object.values(navTabs).find((item) => pageName === item.id && item.pageId == null)
+ ),
+ [pageName, navTabs]
+ );
+
+ const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab());
+
+ useEffect(() => {
+ const currentTabSelected = mapLocationToTab();
+
+ if (currentTabSelected !== selectedTabId) {
+ setSelectedTabId(currentTabSelected);
+ }
+
+ // we do need navTabs in case the selectedTabId appears after initial load (ex. checking permissions for anomalies)
+ }, [pageName, navTabs, mapLocationToTab, selectedTabId]);
+
+ const navItems = usePrimaryNavigationItems({
+ filters,
+ navTabs,
+ query,
+ savedQuery,
+ selectedTabId,
+ sourcerer,
+ timeline,
+ timerange,
+ });
+
+ return {
+ name: translatedNavTitle,
+ icon: 'logoSecurity',
+ items: navItems,
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx
index 30b89086fb99c..051c1bd8ae5cb 100644
--- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx
@@ -5,14 +5,10 @@
* 2.0.
*/
-import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui';
+import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon } from '@elastic/eui';
import styled, { createGlobalStyle } from 'styled-components';
-import {
- GLOBAL_HEADER_HEIGHT,
- FULL_SCREEN_TOGGLED_CLASS_NAME,
- SCROLLING_DISABLED_CLASS_NAME,
-} from '../../../../common/constants';
+import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
export const SecuritySolutionAppWrapper = styled.div`
display: flex;
@@ -27,25 +23,6 @@ SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper';
and `EuiPopover`, `EuiToolTip` global styles
*/
export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>`
- // fixes double scrollbar on views with EventsTable
- #kibana-body {
- overflow: hidden;
- }
-
- div.kbnAppWrapper {
- background-color: rgba(0,0,0,0);
- }
-
- div.application {
- background-color: rgba(0,0,0,0);
-
- // Security App wrapper
- > div {
- display: flex;
- flex: 1 1 auto;
- }
- }
-
.euiPopover__panel.euiPopover__panel-isOpen {
z-index: 9900 !important;
min-width: 24px;
@@ -82,10 +59,6 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar
${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`};
}
- .${SCROLLING_DISABLED_CLASS_NAME} ${SecuritySolutionAppWrapper} {
- max-height: calc(100vh - ${GLOBAL_HEADER_HEIGHT}px);
- }
-
/*
EuiScreenReaderOnly has a default 1px height and width. These extra pixels
were adding additional height to every table row in the alerts table on the
@@ -122,96 +95,6 @@ export const DescriptionListStyled = styled(EuiDescriptionList)`
DescriptionListStyled.displayName = 'DescriptionListStyled';
-export const PageContainer = styled.div`
- display: flex;
- flex-direction: column;
- align-items: stretch;
- background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
- height: 100%;
- padding: 1rem;
- overflow: hidden;
- margin: 0px;
-`;
-
-PageContainer.displayName = 'PageContainer';
-
-export const PageContent = styled.div`
- flex: 1 1 auto;
- height: 100%;
- position: relative;
- overflow-y: hidden;
- background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
- margin-top: 62px;
-`;
-
-PageContent.displayName = 'PageContent';
-
-export const FlexPage = styled(EuiPage)`
- flex: 1 0 0;
-`;
-
-FlexPage.displayName = 'FlexPage';
-
-export const PageHeader = styled.div`
- background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
- display: flex;
- user-select: none;
- padding: 1rem 1rem 0rem 1rem;
- width: 100vw;
- position: fixed;
-`;
-
-PageHeader.displayName = 'PageHeader';
-
-export const FooterContainer = styled.div`
- flex: 0;
- bottom: 0;
- color: #666;
- left: 0;
- position: fixed;
- text-align: left;
- user-select: none;
- width: 100%;
- background-color: #f5f7fa;
- padding: 16px;
- border-top: 1px solid #d3dae6;
-`;
-
-FooterContainer.displayName = 'FooterContainer';
-
-export const PaneScrollContainer = styled.div`
- height: 100%;
- overflow-y: scroll;
- > div:last-child {
- margin-bottom: 3rem;
- }
-`;
-
-PaneScrollContainer.displayName = 'PaneScrollContainer';
-
-export const Pane = styled.div`
- height: 100%;
- overflow: hidden;
- user-select: none;
-`;
-
-Pane.displayName = 'Pane';
-
-export const PaneHeader = styled.div`
- display: flex;
-`;
-
-PaneHeader.displayName = 'PaneHeader';
-
-export const Pane1FlexContent = styled.div`
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- height: 100%;
-`;
-
-Pane1FlexContent.displayName = 'Pane1FlexContent';
-
export const CountBadge = (styled(EuiBadge)`
margin-left: 5px;
` as unknown) as typeof EuiBadge;
diff --git a/x-pack/plugins/security_solution/public/common/components/page_wrapper/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/page_wrapper/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000000000..5da587f23693b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/page_wrapper/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SecuritySolutionPageWrapper it renders 1`] = `
+
+
+ Test page
+
+
+`;
diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.test.tsx
similarity index 65%
rename from x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx
rename to x-pack/plugins/security_solution/public/common/components/page_wrapper/index.test.tsx
index 3ec1e44205dd3..f6ebf2a90abb4 100644
--- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.test.tsx
@@ -9,18 +9,18 @@ import { shallow } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../mock';
-import { WrapperPage } from './index';
+import { SecuritySolutionPageWrapper } from './index';
-describe('WrapperPage', () => {
+describe('SecuritySolutionPageWrapper', () => {
test('it renders', () => {
const wrapper = shallow(
-
+
{'Test page'}
-
+
);
- expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot();
+ expect(wrapper.find('Memo(SecuritySolutionPageWrapperComponent)')).toMatchSnapshot();
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.tsx
similarity index 68%
rename from x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx
rename to x-pack/plugins/security_solution/public/common/components/page_wrapper/index.tsx
index a3eb76a2728bf..82e0ded264b06 100644
--- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.tsx
@@ -15,30 +15,26 @@ import { gutterTimeline } from '../../lib/helpers';
import { AppGlobalStyle } from '../page/index';
const Wrapper = styled.div`
- padding: ${(props) => `${props.theme.eui.paddingSizes.l}`};
-
- &.siemWrapperPage--fullHeight {
+ &.securitySolutionWrapper--fullHeight {
height: 100%;
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
-
- &.siemWrapperPage--noPadding {
+ &.securitySolutionWrapper--noPadding {
padding: 0;
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
-
- &.siemWrapperPage--withTimeline {
+ &.securitySolutionWrapper--withTimeline {
padding-bottom: ${gutterTimeline};
}
`;
Wrapper.displayName = 'Wrapper';
-interface WrapperPageProps {
+interface SecuritySolutionPageWrapperProps {
children: React.ReactNode;
restrictWidth?: boolean | number | string;
style?: Record;
@@ -46,24 +42,19 @@ interface WrapperPageProps {
noTimeline?: boolean;
}
-const WrapperPageComponent: React.FC = ({
- children,
- className,
- style,
- noPadding,
- noTimeline,
- ...otherProps
-}) => {
+const SecuritySolutionPageWrapperComponent: React.FC<
+ SecuritySolutionPageWrapperProps & CommonProps
+> = ({ children, className, style, noPadding, noTimeline, ...otherProps }) => {
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
useEffect(() => {
setGlobalFullScreen(false); // exit full screen mode on page load
}, [setGlobalFullScreen]);
const classes = classNames(className, {
- siemWrapperPage: true,
- 'siemWrapperPage--noPadding': noPadding,
- 'siemWrapperPage--withTimeline': !noTimeline,
- 'siemWrapperPage--fullHeight': globalFullScreen,
+ securitySolutionWrapper: true,
+ 'securitySolutionWrapper--noPadding': noPadding,
+ 'securitySolutionWrapper--withTimeline': !noTimeline,
+ 'securitySolutionWrapper--fullHeight': globalFullScreen,
});
return (
@@ -74,4 +65,4 @@ const WrapperPageComponent: React.FC = ({
);
};
-export const WrapperPage = React.memo(WrapperPageComponent);
+export const SecuritySolutionPageWrapper = React.memo(SecuritySolutionPageWrapperComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/panel/index.tsx b/x-pack/plugins/security_solution/public/common/components/panel/index.tsx
index 652d22409cb0c..802fd4c7f44a6 100644
--- a/x-pack/plugins/security_solution/public/common/components/panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/panel/index.tsx
@@ -25,7 +25,7 @@ import { EuiPanel } from '@elastic/eui';
* Ref: https://www.styled-components.com/docs/faqs#why-am-i-getting-html-attribute-warnings
* Ref: https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html
*/
-export const Panel = styled(({ loading, ...props }) => )`
+export const Panel = styled(({ loading, ...props }) => )`
position: relative;
${({ loading }) =>
loading &&
diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx
index 5b4a8f67aa361..2d8d55a5c943f 100644
--- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx
@@ -222,7 +222,7 @@ export const StatItemsComponent = React.memo(
return (
-
+
diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx
index 8c2b97a4b8b38..c122138f9547a 100644
--- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx
@@ -18,6 +18,9 @@ import {
import { TestProviders } from '../../mock';
import { getEmptyValue } from '../empty_value';
import { useMountAppended } from '../../utils/use_mount_appended';
+
+jest.mock('../../lib/kibana');
+
describe('Table Helpers', () => {
const items = ['item1', 'item2', 'item3'];
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts
index 70e095c88576f..04ceafde7ef74 100644
--- a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts
+++ b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts
@@ -8,10 +8,10 @@
import type React from 'react';
import uuid from 'uuid';
import { isError } from 'lodash/fp';
+import { isAppError } from '@kbn/securitysolution-t-grid';
import { AppToast, ActionToaster } from './';
import { isToasterError } from './errors';
-import { isAppError } from '../../utils/api';
/**
* Displays an error toast for the provided title and message
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
index 005602738f376..4f6834e84d83a 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
@@ -18,17 +18,11 @@ import {
createSecuritySolutionStorageMock,
mockIndexPattern,
} from '../../mock';
-import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { createStore, State } from '../../store';
import { Props } from './top_n';
import { StatefulTopN } from '.';
-import {
- ManageGlobalTimeline,
- getTimelineDefaults,
-} from '../../../timelines/components/manage_timeline';
import { TimelineId } from '../../../../common/types/timeline';
-import { coreMock } from '../../../../../../../src/core/public/mocks';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -45,8 +39,6 @@ jest.mock('../link_to');
jest.mock('../../lib/kibana');
jest.mock('../../../timelines/store/timeline/actions');
-const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
-
const field = 'process.name';
const value = 'nice';
@@ -175,9 +167,7 @@ describe('StatefulTopN', () => {
beforeEach(() => {
wrapper = mount(
-
-
-
+
);
});
@@ -244,26 +234,16 @@ describe('StatefulTopN', () => {
});
describe('rendering in a timeline context', () => {
- let filterManager: FilterManager;
let wrapper: ReactWrapper;
beforeEach(() => {
- filterManager = new FilterManager(mockUiSettingsForFilterManager);
- const manageTimelineForTesting = {
- [TimelineId.active]: {
- ...getTimelineDefaults(TimelineId.active),
- filterManager,
- },
- };
testProps = {
...testProps,
timelineId: TimelineId.active,
};
wrapper = mount(
-
-
-
+
);
});
@@ -320,25 +300,13 @@ describe('StatefulTopN', () => {
});
describe('rendering in a NON-active timeline context', () => {
test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, async () => {
- const filterManager = new FilterManager(mockUiSettingsForFilterManager);
-
- const manageTimelineForTesting = {
- [TimelineId.active]: {
- ...getTimelineDefaults(TimelineId.active),
- filterManager,
- documentType: 'alerts',
- },
- };
-
testProps = {
...testProps,
timelineId: TimelineId.detectionsPage,
};
const wrapper = mount(
-
-
-
+
);
await waitFor(() => {
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
index a2d5076031328..8a7c6bcb4a9b5 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
@@ -29,7 +29,6 @@ import { SecurityPageName } from '../../../../common/constants';
export const dispatchSetInitialStateFromUrl = (
dispatch: Dispatch
): DispatchSetInitialStateFromUrl => ({
- detailName,
filterManager,
indexPattern,
pageName,
diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx
index a8868436d9689..c867862e690bd 100644
--- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx
@@ -6,13 +6,13 @@
*/
import { EuiPopover } from '@elastic/eui';
+import {
+ HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME,
+ IS_DRAGGING_CLASS_NAME,
+} from '@kbn/securitysolution-t-grid';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
-import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers';
-
-export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show';
-
/**
* To avoid expensive changes to the DOM, delay showing the popover menu
*/
diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index 89ed2f45a6bf1..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`WrapperPage it renders 1`] = `
-
-
- Test page
-
-
-`;
diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
index 3e690e50b04b1..4f558412576b4 100644
--- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
@@ -83,7 +83,7 @@ export const useTimelineLastEventTime = ({
TimelineEventsLastEventTimeRequestOptions,
TimelineEventsLastEventTimeStrategyResponse
>(request, {
- strategy: 'securitySolutionTimelineSearchStrategy',
+ strategy: 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
index 1c17f95bb6ba0..3bc92dafd351f 100644
--- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
@@ -151,7 +151,7 @@ export const useFetchIndex = (
{ indices: iNames, onlyCheckIfIndicesExist },
{
abortSignal: abortCtrl.current.signal,
- strategy: 'securitySolutionIndexFields',
+ strategy: 'indexFields',
}
)
.subscribe({
@@ -235,7 +235,7 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
{ indices: indicesName, onlyCheckIfIndicesExist: false },
{
abortSignal: abortCtrl.current.signal,
- strategy: 'securitySolutionIndexFields',
+ strategy: 'indexFields',
}
)
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
index da6b41080c1c7..6c5caa25a1f96 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
@@ -7,9 +7,10 @@
import { renderHook } from '@testing-library/react-hooks';
import { IEsError } from 'src/plugins/data/public';
+import { KibanaError, SecurityAppError } from '@kbn/securitysolution-t-grid';
import { useToasts } from '../lib/kibana';
-import { KibanaError, SecurityAppError } from '../utils/api';
+
import {
appErrorToErrorStack,
convertErrorToEnumerable,
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
index 61b20e137f870..0c2721e6ad416 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
@@ -7,11 +7,17 @@
import { useCallback, useRef } from 'react';
import { isString } from 'lodash/fp';
+import {
+ AppError,
+ isAppError,
+ isKibanaError,
+ isSecurityAppError,
+} from '@kbn/securitysolution-t-grid';
+
import { IEsError, isEsError } from '../../../../../../src/plugins/data/public';
import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public';
import { useToasts } from '../lib/kibana';
-import { AppError, isAppError, isKibanaError, isSecurityAppError } from '../utils/api';
export type UseAppToasts = Pick & {
api: ToastsStart;
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx
index 5b5877a4c2ded..8e8d73ff12849 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx
@@ -11,10 +11,10 @@ import { createPortalNode } from 'react-reverse-portal';
/**
* A singleton portal for rendering content in the global header
*/
-const globalHeaderPortalNodeSingleton = createPortalNode();
+const globalKQLHeaderPortalNodeSingleton = createPortalNode();
export const useGlobalHeaderPortal = () => {
- const [globalHeaderPortalNode] = useState(globalHeaderPortalNodeSingleton);
+ const [globalKQLHeaderPortalNode] = useState(globalKQLHeaderPortalNodeSingleton);
- return { globalHeaderPortalNode };
+ return { globalKQLHeaderPortalNode };
};
diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx
index 1baa57166de3f..2f5afc8a44489 100644
--- a/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx
+++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx
@@ -6,9 +6,10 @@
*/
import { EuiToolTip } from '@elastic/eui';
+
import React from 'react';
-import { TooltipWithKeyboardShortcut } from '../../components/accessibility/tooltip_with_keyboard_shortcut';
+import { TooltipWithKeyboardShortcut } from '../../components/accessibility';
import * as i18n from '../../components/drag_and_drop/translations';
import { Clipboard } from './clipboard';
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
index eb0ae1ae1dee9..09c3d2537e272 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
@@ -6,6 +6,10 @@
*/
import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks';
+
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { createTGridMocks } from '../../../../../../timelines/public/mock';
+
import {
createKibanaContextProviderMock,
createUseUiSettingMock,
@@ -30,14 +34,24 @@ export const useKibana = jest.fn().mockReturnValue({
})),
})),
},
+ query: {
+ ...mockStartServicesMock.data.query,
+ filterManager: {
+ addFilters: jest.fn(),
+ getFilters: jest.fn(),
+ getUpdates$: jest.fn().mockReturnValue({ subscribe: jest.fn() }),
+ setAppFilters: jest.fn(),
+ },
+ },
},
+ timelines: createTGridMocks(),
},
});
export const useUiSetting = jest.fn(createUseUiSettingMock());
export const useUiSetting$ = jest.fn(createUseUiSetting$Mock());
export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http);
export const useTimeZone = jest.fn();
-export const useDateFormat = jest.fn();
+export const useDateFormat = jest.fn().mockReturnValue('MMM D, YYYY @ HH:mm:ss.SSS');
export const useBasePath = jest.fn(() => '/test/base/path');
export const useToasts = jest
.fn()
diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
index 557c04e4e8a47..316f8b6214d1e 100644
--- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
@@ -43,6 +43,7 @@ export const mockGlobalState: State = {
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
+ tGridEnabled: false,
},
},
hosts: {
diff --git a/x-pack/plugins/security_solution/public/common/mock/header.ts b/x-pack/plugins/security_solution/public/common/mock/header.ts
index ae7d3c9e576a8..029ddb00d1832 100644
--- a/x-pack/plugins/security_solution/public/common/mock/header.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/header.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { ColumnHeaderOptions } from '../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../common';
import { defaultColumnHeaderType } from '../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx
index 7604732f90203..7dae3e671d271 100644
--- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx
@@ -15,7 +15,7 @@ import {
EuiPopoverTitle,
EuiSpacer,
} from '@elastic/eui';
-import { ControlColumnProps } from '../../timelines/components/timeline/body/control_columns';
+import { ControlColumnProps } from '../../../common/types/timeline';
const SelectionHeaderCell = () => {
return (
diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts
index 30951b81611db..e0f8e651a5821 100644
--- a/x-pack/plugins/security_solution/public/common/mock/utils.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts
@@ -5,12 +5,20 @@
* 2.0.
*/
+import { AnyAction, Reducer } from 'redux';
+import reduceReducers from 'reduce-reducers';
+
+import { tGridReducer } from '../../../../timelines/public';
+
import { hostsReducer } from '../../hosts/store';
import { networkReducer } from '../../network/store';
import { timelineReducer } from '../../timelines/store/timeline/reducer';
import { managementReducer } from '../../management/store/reducer';
import { ManagementPluginReducer } from '../../management';
import { SubPluginsInitReducer } from '../store';
+import { mockGlobalState } from './global_state';
+import { TimelineState } from '../../timelines/store/timeline/types';
+import { defaultHeaders } from '../../timelines/components/timeline/body/column_headers/default_headers';
interface Global extends NodeJS.Global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -19,10 +27,32 @@ interface Global extends NodeJS.Global {
export const globalNode: Global = global;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const combineTimelineReducer = reduceReducers(
+ {
+ ...mockGlobalState.timeline,
+ timelineById: {
+ ...mockGlobalState.timeline.timelineById,
+ test: {
+ ...mockGlobalState.timeline.timelineById.test,
+ defaultColumns: defaultHeaders,
+ loadingText: 'events',
+ footerText: 'events',
+ documentType: '',
+ selectAll: false,
+ queryFields: [],
+ unit: (n: number) => n,
+ },
+ },
+ },
+ tGridReducer,
+ timelineReducer
+) as Reducer;
+
export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = {
hosts: hostsReducer,
network: networkReducer,
- timeline: timelineReducer,
+ timeline: combineTimelineReducer,
/**
* These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture,
* they are cast to mutable versions here.
diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts
index e784f6cebae17..5791a4940cbed 100644
--- a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts
+++ b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts
@@ -60,6 +60,7 @@ export interface GlobalGenericQuery {
isInspected: boolean;
loading: boolean;
selectedInspectIndex: number;
+ invalidKqlQuery?: Error;
}
export interface GlobalGraphqlQuery extends GlobalGenericQuery {
diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts
index fbf4caad9793d..21e833abe1f9b 100644
--- a/x-pack/plugins/security_solution/public/common/store/types.ts
+++ b/x-pack/plugins/security_solution/public/common/store/types.ts
@@ -37,18 +37,6 @@ export type StoreState = HostsPluginState &
*/
export type State = CombinedState;
-export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
-
-export interface KueryFilterQuery {
- kind: KueryFilterQueryKind;
- expression: string;
-}
-
-export interface SerializedFilterQuery {
- kuery: KueryFilterQuery | null;
- serializedQuery: string;
-}
-
/**
* like redux's `MiddlewareAPI` but `getState` returns an `Immutable` version of
* state and `dispatch` accepts `Immutable` versions of actions.
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx
index 91b5a10684405..d766104e356eb 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx
@@ -298,7 +298,7 @@ export const AlertsHistogramPanel = memo(
return (
-
+
{
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
index 02a815bc59f3b..9a142f6cba247 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
@@ -6,11 +6,11 @@
*/
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
-import { RowRendererId } from '../../../../common/types/timeline';
+import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter } from '../../../../../../../src/plugins/data/common/es_query';
-import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model';
+import { SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { columns } from '../../configurations/security_solution_detections/columns';
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index f20754fc446d6..7980160fea76c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -8,11 +8,11 @@
import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter, esQuery } from '../../../../../../../src/plugins/data/public';
-import { TimelineIdLiteral } from '../../../../common/types/timeline';
+import { RowRendererId, TimelineIdLiteral } from '../../../../common/types/timeline';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
import { HeaderSection } from '../../../common/components/header_section';
@@ -23,8 +23,6 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
import { TimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
-
import { updateAlertStatusAction } from './actions';
import {
requiredFieldsForActions,
@@ -95,6 +93,7 @@ export const AlertsTableComponent: React.FC = ({
timelineId,
to,
}) => {
+ const dispatch = useDispatch();
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
const [filterGroup, setFilterGroup] = useState(FILTER_OPEN);
const {
@@ -106,7 +105,6 @@ export const AlertsTableComponent: React.FC = ({
const kibana = useKibana();
const [, dispatchToaster] = useStateToaster();
const { addWarning } = useAppToasts();
- const { initializeTimeline, setSelectAll } = useManageTimeline();
// TODO: Once we are past experimental phase this code should be removed
const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled');
@@ -195,14 +193,16 @@ export const AlertsTableComponent: React.FC = ({
// Catches state change isSelectAllChecked->false upon user selection change to reset utility bar
useEffect(() => {
if (isSelectAllChecked) {
- setSelectAll({
- id: timelineId,
- selectAll: false,
- });
+ dispatch(
+ timelineActions.setTGridSelectAll({
+ id: timelineId,
+ selectAll: false,
+ })
+ );
} else {
setShowClearSelectionAction(false);
}
- }, [isSelectAllChecked, setSelectAll, timelineId]);
+ }, [dispatch, isSelectAllChecked, timelineId]);
// Callback for when open/closed filter changes
const onFilterGroupChangedCallback = useCallback(
@@ -218,23 +218,27 @@ export const AlertsTableComponent: React.FC = ({
// Callback for clearing entire selection from utility bar
const clearSelectionCallback = useCallback(() => {
clearSelected!({ id: timelineId });
- setSelectAll({
- id: timelineId,
- selectAll: false,
- });
+ dispatch(
+ timelineActions.setTGridSelectAll({
+ id: timelineId,
+ selectAll: false,
+ })
+ );
setShowClearSelectionAction(false);
- }, [clearSelected, setSelectAll, setShowClearSelectionAction, timelineId]);
+ }, [clearSelected, dispatch, timelineId]);
// Callback for selecting all events on all pages from utility bar
// Dispatches to stateful_body's selectAll via TimelineTypeContext props
// as scope of response data required to actually set selectedEvents
const selectAllOnAllPagesCallback = useCallback(() => {
- setSelectAll({
- id: timelineId,
- selectAll: true,
- });
+ dispatch(
+ timelineActions.setTGridSelectAll({
+ id: timelineId,
+ selectAll: true,
+ })
+ );
setShowClearSelectionAction(true);
- }, [setSelectAll, setShowClearSelectionAction, timelineId]);
+ }, [dispatch, timelineId]);
const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback(
async (
@@ -330,22 +334,22 @@ export const AlertsTableComponent: React.FC = ({
: alertsDefaultModel;
useEffect(() => {
- initializeTimeline({
- defaultModel: {
- ...defaultTimelineModel,
- columns,
- },
- documentType: i18n.ALERTS_DOCUMENT_TYPE,
- filterManager,
- footerText: i18n.TOTAL_COUNT_OF_ALERTS,
- id: timelineId,
- loadingText: i18n.LOADING_ALERTS,
- selectAll: false,
- queryFields: requiredFieldsForActions,
- title: '',
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ defaultColumns: columns,
+ documentType: i18n.ALERTS_DOCUMENT_TYPE,
+ excludedRowRendererIds: defaultTimelineModel.excludedRowRendererIds as RowRendererId[],
+ filterManager,
+ footerText: i18n.TOTAL_COUNT_OF_ALERTS,
+ id: timelineId,
+ loadingText: i18n.LOADING_ALERTS,
+ selectAll: false,
+ queryFields: requiredFieldsForActions,
+ title: '',
+ showCheckboxes: true,
+ })
+ );
+ }, [dispatch, defaultTimelineModel, filterManager, timelineId]);
const headerFilterGroup = useMemo(
() => ,
@@ -354,7 +358,7 @@ export const AlertsTableComponent: React.FC = ({
if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) {
return (
-
+
diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx
index fd0be8e002193..3b41c9280998b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx
@@ -6,6 +6,7 @@
*/
import React, { memo } from 'react';
+import { EuiSpacer } from '@elastic/eui';
import { CallOutMessage, CallOutPersistentSwitcher } from '../../../../common/components/callouts';
import { useUserData } from '../../user_info';
@@ -33,20 +34,22 @@ const needAdminForUpdateRulesMessage: CallOutMessage = {
* hasIndexManage is also true, then the user should be performing the update on the page which is
* why we do not show it for that condition.
*/
-const NeedAdminForUpdateCallOutComponent = (): JSX.Element => {
+const NeedAdminForUpdateCallOutComponent = (): JSX.Element | null => {
const [{ signalIndexMappingOutdated, hasIndexManage }] = useUserData();
const signalIndexMappingIsOutdated =
signalIndexMappingOutdated != null && signalIndexMappingOutdated;
const userDoesntHaveIndexManage = hasIndexManage != null && !hasIndexManage;
-
- return (
-
- );
+ const shouldShowCallout = signalIndexMappingIsOutdated && userDoesntHaveIndexManage;
+
+ // Passing shouldShowCallout to the condition param will end up with an unecessary spacer being rendered
+ return shouldShowCallout ? (
+ <>
+
+
+ >
+ ) : null;
};
export const NeedAdminForUpdateRulesCallOut = memo(NeedAdminForUpdateCallOutComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx
index f21c66380f30a..7b483930db505 100644
--- a/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiCallOut, EuiButton } from '@elastic/eui';
+import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui';
import React, { memo, useCallback, useState } from 'react';
import * as i18n from './translations';
@@ -15,12 +15,15 @@ const NoApiIntegrationKeyCallOutComponent = () => {
const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]);
return showCallOut ? (
-
- {i18n.NO_API_INTEGRATION_KEY_CALLOUT_MSG}
-
- {i18n.DISMISS_CALLOUT}
-
-
+ <>
+
+ {i18n.NO_API_INTEGRATION_KEY_CALLOUT_MSG}
+
+ {i18n.DISMISS_CALLOUT}
+
+
+
+ >
) : null;
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
index a09afa3ca2164..c1078e1ba77e7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
@@ -82,7 +82,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({
);
return (
-
+
{loading && (
<>
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx
index f9e6031d826ca..ac9a153ad76bf 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx
@@ -24,7 +24,7 @@ const MyPanel = styled(EuiPanel)`
MyPanel.displayName = 'MyPanel';
const StepPanelComponent: React.FC = ({ children, loading, title }) => (
-
+
{loading && }
{children}
diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx
index dbad1c57fda77..3d81735122e73 100644
--- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx
@@ -216,7 +216,7 @@ export const ValueListsModalComponent: React.FC = ({
-
+
{i18n.TABLE_TITLE}
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts
index 8cbb532501a2c..70d2237a535eb 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts
@@ -6,10 +6,9 @@
*/
import { EuiDataGridColumn } from '@elastic/eui';
-
+import { ColumnHeaderOptions } from '../../../../../common';
import { defaultColumnHeaderType } from '../../../../timelines/components/timeline/body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../../timelines/components/timeline/body/constants';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
import * as i18n from '../../../components/alerts_table/translations';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx
index 9c2114a4ef085..7db75d3a73d90 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx
@@ -15,10 +15,12 @@ import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../com
import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../common';
import { RenderCellValue } from '.';
+jest.mock('../../../../common/lib/kibana/');
+
describe('RenderCellValue', () => {
const columnId = '@timestamp';
const eventId = '_id-123';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts
index 96d2d870b1270..3365ce5432940 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts
@@ -6,10 +6,9 @@
*/
import { EuiDataGridColumn } from '@elastic/eui';
-
+import { ColumnHeaderOptions } from '../../../../../common';
import { defaultColumnHeaderType } from '../../../../timelines/components/timeline/body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../../timelines/components/timeline/body/constants';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
import * as i18n from '../../../components/alerts_table/translations';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx
index aa4eb543a3d9b..a8f295df2540d 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx
@@ -15,9 +15,11 @@ import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../com
import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
import { RenderCellValue } from '.';
+import { ColumnHeaderOptions } from '../../../../../common';
+
+jest.mock('../../../../common/lib/kibana/');
describe('RenderCellValue', () => {
const columnId = '@timestamp';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts
index 23a0740294e84..7f46c839ffe62 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts
+++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts
@@ -6,13 +6,13 @@
*/
import { EuiDataGridColumn } from '@elastic/eui';
+import { ColumnHeaderOptions } from '../../../../common';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import * as i18n from '../../components/alerts_table/translations';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx
index 18350c102c049..965ee913a1daa 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx
@@ -9,16 +9,18 @@ import { mount } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../common';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { DragDropContextWrapper } from '../../../common/components/drag_and_drop/drag_drop_context_wrapper';
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock';
import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { RenderCellValue } from '.';
+jest.mock('../../../common/lib/kibana');
+
describe('RenderCellValue', () => {
const columnId = '@timestamp';
const eventId = '_id-123';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
index 84eaf8e3aa93c..6f8d938dd987e 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
@@ -6,13 +6,13 @@
*/
import { useEffect, useState } from 'react';
+import { isSecurityAppError } from '@kbn/securitysolution-t-grid';
import { DEFAULT_ALERTS_INDEX } from '../../../../../common/constants';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { createSignalIndex, getSignalIndex } from './api';
import * as i18n from './translations';
-import { isSecurityAppError } from '../../../../common/utils/api';
import { useAlertsPrivileges } from './use_alerts_privileges';
type Func = () => Promise;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx
index 8e231f0d1fdbb..d55d171708963 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx
@@ -6,10 +6,9 @@
*/
import { useEffect, useState, useCallback } from 'react';
-
+import { isSecurityAppError } from '@kbn/securitysolution-t-grid';
import { useReadListIndex, useCreateListIndex } from '@kbn/securitysolution-list-hooks';
import { useHttp, useKibana } from '../../../../common/lib/kibana';
-import { isSecurityAppError } from '../../../../common/utils/api';
import * as i18n from './translations';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useListsPrivileges } from './use_lists_privileges';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
index f848b71cf7bd3..4f524886935cd 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
@@ -6,8 +6,8 @@
*/
import { useEffect, useRef, useState } from 'react';
+import { isNotFoundError } from '@kbn/securitysolution-t-grid';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-import { isNotFoundError } from '../../../../common/utils/api';
import { RuleStatusRowItemType } from '../../../pages/detection_engine/rules/all/columns';
import { getRuleStatusById, getRulesStatusByIds } from './api';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
index 4a39e486b6fd5..abd5a2781c8a7 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
@@ -6,11 +6,11 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
+import { SecurityAppError } from '@kbn/securitysolution-t-grid';
import { useRuleWithFallback } from './use_rule_with_fallback';
import * as api from './api';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-import { SecurityAppError } from '../../../../common/utils/api';
jest.mock('./api');
jest.mock('../alerts/api');
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
index 11c30547848c3..da56275280f65 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
@@ -6,9 +6,9 @@
*/
import { useCallback, useEffect, useMemo } from 'react';
+import { isNotFoundError } from '@kbn/securitysolution-t-grid';
import { useAsync, withOptionalSignal } from '@kbn/securitysolution-hook-utils';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-import { isNotFoundError } from '../../../../common/utils/api';
import { useQueryAlerts } from '../alerts/use_query';
import { fetchRuleById } from './api';
import { transformInput } from './transforms';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
index 8ae7e4fb2852b..0c12d8256d66d 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
@@ -11,18 +11,18 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
+import { isTab } from '../../../../../timelines/public';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { SecurityPageName } from '../../../app/types';
import { TimelineId } from '../../../../common/types/timeline';
import { useGlobalTime } from '../../../common/containers/use_global_time';
-import { isTab } from '../../../common/components/accessibility/helpers';
import { UpdateDateRange } from '../../../common/components/charts/common';
import { FiltersGlobal } from '../../../common/components/filters_global';
import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
import { SiemSearchBar } from '../../../common/components/search_bar';
-import { WrapperPage } from '../../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
import { inputsSelectors } from '../../../common/store/inputs';
import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions';
import { SpyRoute } from '../../../common/utils/route/spy_routes';
@@ -197,22 +197,22 @@ const DetectionEnginePageComponent = () => {
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
return (
-
+
-
+
);
}
if (!loading && (isSignalIndexExists === false || needsListsConfiguration)) {
return (
-
+
-
+
);
}
@@ -228,7 +228,7 @@ const DetectionEnginePageComponent = () => {
-
+
{
onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsCallback}
to={to}
/>
-
+
) : (
-
+
-
+
)}
>
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
index dd3549ea20d36..8cc3113a5706a 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
@@ -42,6 +42,9 @@ describe('ExceptionListsTable', () => {
addError: jest.fn(),
},
},
+ timelines: {
+ getLastUpdated: () => null,
+ },
},
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
index 7f734b10fd020..f38bde4839f18 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
@@ -26,7 +26,6 @@ import { Loader } from '../../../../../../common/components/loader';
import { Panel } from '../../../../../../common/components/panel';
import * as i18n from './translations';
import { AllRulesUtilityBar } from '../utility_bar';
-import { LastUpdatedAt } from '../../../../../../common/components/last_updated';
import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns';
import { useAllExceptionLists } from './use_all_exception_lists';
import { ReferenceErrorModal } from '../../../../../components/value_lists_management_modal/reference_error_modal';
@@ -62,7 +61,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
export const ExceptionListsTable = React.memo(
({ formatUrl, history, hasPermissions, loading }) => {
const {
- services: { http, notifications },
+ services: { http, notifications, timelines },
} = useKibana();
const { exportExceptionList, deleteExceptionList } = useApi(http);
@@ -78,6 +77,7 @@ export const ExceptionListsTable = React.memo(
namespaceTypes: ['single', 'agnostic'],
notifications,
showTrustedApps: false,
+ showEventFilters: false,
});
const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists(
{
@@ -344,7 +344,7 @@ export const ExceptionListsTable = React.memo(
}
+ subtitle={timelines.getLastUpdated({ showUpdating: loading, updatedAt: lastUpdated })}
>
{!initLoading && }
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
index 8fd82a495e52f..2ec34aaece60b 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
@@ -47,7 +47,6 @@ import { hasMlAdminPermissions } from '../../../../../../common/machine_learning
import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license';
import { isBoolean } from '../../../../../common/utils/privileges';
import { AllRulesUtilityBar } from './utility_bar';
-import { LastUpdatedAt } from '../../../../../common/components/last_updated';
import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants';
import { AllRulesTabs } from '.';
import { useValueChanged } from '../../../../../common/hooks/use_value_changed';
@@ -104,6 +103,7 @@ export const RulesTables = React.memo(
application: {
capabilities: { actions },
},
+ timelines,
},
} = useKibana();
@@ -473,12 +473,10 @@ export const RulesTables = React.memo(
split
growLeftSplit={false}
title={i18n.ALL_RULES}
- subtitle={
-
- }
+ subtitle={timelines.getLastUpdated({
+ showUpdating: loading || isLoadingRules || isLoadingRulesStatuses,
+ updatedAt: lastUpdated,
+ })}
>
{shouldShowRulesTable && (
{
return (
<>
-
+
{
text: i18n.BACK_TO_RULES,
pageId: SecurityPageName.detections,
}}
- border
isLoading={isLoading || loading}
title={i18n.PAGE_TITLE}
/>
-
+
{
-
+
{
-
+
{
-
+
{
-
+
>
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
index 417e1c989ce9b..2fedd6160af2c 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
@@ -29,7 +29,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => {
const [loading, ruleStatus] = useRuleStatus(id);
if (loading) {
return (
-
+
@@ -60,7 +60,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => {
},
];
return (
-
+
{
-
+
{
/>
)}
{ruleDetailTab === RuleDetailTabs.failures && }
-
+
) : (
-
+
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
index 2d751459eb12f..41710a822e539 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
@@ -21,7 +21,7 @@ import { useParams, useHistory } from 'react-router-dom';
import { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules';
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
-import { WrapperPage } from '../../../../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../../../../common/components/page_wrapper';
import {
getRuleDetailsUrl,
getDetectionEngineUrl,
@@ -335,7 +335,7 @@ const EditRulePageComponent: FC = () => {
return (
<>
-
+
{
-
+
>
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
index 8bacb10444a7d..29fd8e2e8b247 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
@@ -16,7 +16,7 @@ import {
getCreateRuleUrl,
} from '../../../../common/components/link_to/redirect_to_detection_engine';
import { DetectionEngineHeaderPage } from '../../../components/detection_engine_header_page';
-import { WrapperPage } from '../../../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { useUserData } from '../../../components/user_info';
@@ -182,7 +182,7 @@ const RulesPageComponent: React.FC = () => {
subtitle={i18n.INITIAL_PROMPT_TEXT}
title={i18n.IMPORT_RULE}
/>
-
+
{
rulesNotUpdated={rulesNotUpdated}
setRefreshRulesData={handleSetRefreshRulesData}
/>
-
+
>
diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx
index b1a0d13ed554b..413b8cda9b6ab 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx
@@ -23,6 +23,8 @@ import { HostsTableType } from '../../../hosts/store/model';
import { HostsTable } from './index';
import { mockData } from './mock';
+jest.mock('../../../common/lib/kibana');
+
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../../common/components/search_bar', () => ({
diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
index 751a2bf5a2055..2cd4ed1f57f84 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
@@ -20,6 +20,8 @@ import { mockData } from './mock';
import { HostsType } from '../../store/model';
import * as i18n from './translations';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx
index 2333d5e9b127c..b51e20b801f40 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx
@@ -19,6 +19,8 @@ import { type } from './utils';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { getHostDetailsPageFilters } from './helpers';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../../../common/components/url_state/normalize_time_range.ts');
jest.mock('../../../common/containers/source', () => ({
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx
index d88e4f048f917..22edd2c19d6bd 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx
@@ -21,11 +21,11 @@ import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_c
import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime';
-import { SiemNavigation } from '../../../common/components/navigation';
+import { SecuritySolutionTabNavigation } from '../../../common/components/navigation';
import { HostsDetailsKpiComponent } from '../../components/kpi_hosts';
import { HostOverview } from '../../../overview/components/host_overview';
import { SiemSearchBar } from '../../../common/components/search_bar';
-import { WrapperPage } from '../../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { useKibana } from '../../../common/lib/kibana';
import { convertToBuildEsQuery } from '../../../common/lib/keury';
@@ -123,7 +123,7 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta
-
+
= ({ detailName, hostDeta
-
@@ -207,14 +207,14 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta
indexPattern={indexPattern}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
/>
-
+
>
) : (
-
+
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx
index f1eab38c56db0..d05b091381cca 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx
@@ -18,7 +18,7 @@ import {
kibanaObservable,
createSecuritySolutionStorageMock,
} from '../../common/mock';
-import { SiemNavigation } from '../../common/components/navigation';
+import { SecuritySolutionTabNavigation } from '../../common/components/navigation';
import { inputsActions } from '../../common/store/inputs';
import { State, createStore } from '../../common/store';
import { Hosts } from './hosts';
@@ -102,7 +102,7 @@ describe('Hosts - rendering', () => {
);
- expect(wrapper.find(SiemNavigation).exists()).toBe(true);
+ expect(wrapper.find(SecuritySolutionTabNavigation).exists()).toBe(true);
});
test('it should add the new filters after init', async () => {
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx
index 57cded85d67cc..7d31d291e75f1 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx
@@ -11,6 +11,7 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
+import { isTab } from '../../../../timelines/public';
import { SecurityPageName } from '../../app/types';
import { UpdateDateRange } from '../../common/components/charts/common';
@@ -18,10 +19,10 @@ import { FiltersGlobal } from '../../common/components/filters_global';
import { HeaderPage } from '../../common/components/header_page';
import { LastEventTime } from '../../common/components/last_event_time';
import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions';
-import { SiemNavigation } from '../../common/components/navigation';
+import { SecuritySolutionTabNavigation } from '../../common/components/navigation';
import { HostsKpiComponent } from '../components/kpi_hosts';
import { SiemSearchBar } from '../../common/components/search_bar';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useGlobalFullScreen } from '../../common/containers/use_full_screen';
import { useGlobalTime } from '../../common/containers/use_global_time';
import { TimelineId } from '../../../common/types/timeline';
@@ -42,7 +43,6 @@ import * as i18n from './translations';
import { filterHostData } from './navigation';
import { hostsModel } from '../store';
import { HostsTableType } from '../store/model';
-import { isTab } from '../../common/components/accessibility/helpers';
import {
onTimelineTabKeyPressed,
resetKeyboardFocus,
@@ -164,10 +164,9 @@ const HostsComponent = () => {
-
+
{
-
+
@@ -207,14 +208,14 @@ const HostsComponent = () => {
from={from}
type={hostsModel.HostsType.page}
/>
-
+
) : (
-
+
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx
index f88709e6e95ac..973dbc41925da 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx
@@ -10,6 +10,7 @@ import { useDispatch } from 'react-redux';
import { TimelineId } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
+import { timelineActions } from '../../../timelines/store/timeline';
import { HostsComponentsQueryProps } from './types';
import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model';
import {
@@ -20,7 +21,6 @@ import { MatrixHistogram } from '../../../common/components/matrix_histogram';
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
import * as i18n from '../translations';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
@@ -64,14 +64,15 @@ const EventsQueryTabBodyComponent: React.FC = ({
startDate,
}) => {
const dispatch = useDispatch();
- const { initializeTimeline } = useManageTimeline();
const { globalFullScreen } = useGlobalFullScreen();
useEffect(() => {
- initializeTimeline({
- id: TimelineId.hostsPageEvents,
- defaultModel: eventsDefaultModel,
- });
- }, [dispatch, initializeTimeline]);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ id: TimelineId.hostsPageEvents,
+ defaultColumns: eventsDefaultModel.columns,
+ })
+ );
+ }, [dispatch]);
useEffect(() => {
return () => {
diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts
index 55262fe039b4e..3d2412b326b54 100644
--- a/x-pack/plugins/security_solution/public/index.ts
+++ b/x-pack/plugins/security_solution/public/index.ts
@@ -8,6 +8,7 @@
import { PluginInitializerContext } from '../../../../src/core/public';
import { Plugin } from './plugin';
import { PluginSetup } from './types';
+export type { TimelineModel } from './timelines/store/timeline/model';
export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context);
diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
index 76acff7847671..3bcbd81621588 100644
--- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
+++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
@@ -11,7 +11,7 @@ import { AdministrationSubTab } from '../types';
import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations';
import { AdministrationRouteSpyState } from '../../common/utils/route/types';
import { GetUrlForApp } from '../../common/components/navigation/types';
-import { ADMINISTRATION } from '../../app/home/translations';
+import { ADMINISTRATION } from '../../app/translations';
import { APP_ID, SecurityPageName } from '../../../common/constants';
const TabNameMappedToI18nKey: Record = {
diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx
index 72a6de2a2de8d..021c900824f8d 100644
--- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx
@@ -9,9 +9,9 @@ import React, { FC, memo } from 'react';
import { EuiPanel, EuiSpacer, CommonProps } from '@elastic/eui';
import styled from 'styled-components';
import { SecurityPageName } from '../../../common/constants';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { HeaderPage } from '../../common/components/header_page';
-import { SiemNavigation } from '../../common/components/navigation';
+import { SecuritySolutionTabNavigation } from '../../common/components/navigation';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { AdministrationSubTab } from '../types';
import {
@@ -46,7 +46,7 @@ export const AdministrationListPage: FC
+
-
- {children}
+ {children}
-
+
);
}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts
index 5b5bac3a0a6e1..949feb2964317 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts
@@ -16,7 +16,7 @@ import {
import { ServerApiError } from '../../../../common/types';
import { GetPolicyListResponse } from '../../policy/types';
import { GetPackagesResponse } from '../../../../../../fleet/common';
-import { EndpointState } from '../types';
+import { EndpointIndexUIQueryParams, EndpointState } from '../types';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/public';
export interface ServerReturnedEndpointList {
@@ -163,12 +163,29 @@ export type EndpointPendingActionsStateChanged = Action<'endpointPendingActionsS
payload: EndpointState['endpointPendingActions'];
};
+export interface EndpointDetailsActivityLogUpdatePaging {
+ type: 'endpointDetailsActivityLogUpdatePaging';
+ payload: {
+ // disable paging when no more data after paging
+ disabled: boolean;
+ page: number;
+ pageSize: number;
+ };
+}
+
+export interface EndpointDetailsFlyoutTabChanged {
+ type: 'endpointDetailsFlyoutTabChanged';
+ payload: { flyoutView: EndpointIndexUIQueryParams['show'] };
+}
+
export type EndpointAction =
| ServerReturnedEndpointList
| ServerFailedToReturnEndpointList
| ServerReturnedEndpointDetails
| ServerFailedToReturnEndpointDetails
| AppRequestedEndpointActivityLog
+ | EndpointDetailsActivityLogUpdatePaging
+ | EndpointDetailsFlyoutTabChanged
| EndpointDetailsActivityLogChanged
| ServerReturnedEndpointPolicyResponse
| ServerFailedToReturnEndpointPolicyResponse
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts
index d43f361a0e6bb..317b735e1169e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts
@@ -19,9 +19,13 @@ export const initialEndpointPageState = (): Immutable => {
loading: false,
error: undefined,
endpointDetails: {
+ flyoutView: undefined,
activityLog: {
- page: 1,
- pageSize: 50,
+ paging: {
+ disabled: false,
+ page: 1,
+ pageSize: 50,
+ },
logData: createUninitialisedResourceState(),
},
hostDetails: {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
index 7f7c5f84f8bff..68dd47362bc38 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
@@ -42,9 +42,13 @@ describe('EndpointList store concerns', () => {
loading: false,
error: undefined,
endpointDetails: {
+ flyoutView: undefined,
activityLog: {
- page: 1,
- pageSize: 50,
+ paging: {
+ disabled: false,
+ page: 1,
+ pageSize: 50,
+ },
logData: { type: 'UninitialisedResourceState' },
},
hostDetails: {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
index 52da30fabf95a..6cf5e989fb645 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
@@ -44,6 +44,7 @@ import {
} from '../../../../common/lib/endpoint_isolation/mocks';
import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator';
import { endpointPageHttpMock } from '../mocks';
+import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
jest.mock('../../policy/store/services/ingest', () => ({
sendGetAgentConfigList: () => Promise.resolve({ items: [] }),
@@ -226,8 +227,16 @@ describe('endpoint list middleware', () => {
const dispatchUserChangedUrl = () => {
dispatchUserChangedUrlToEndpointList({ search: `?${search.split('?').pop()}` });
};
+ const dispatchFlyoutViewChange = () => {
+ dispatch({
+ type: 'endpointDetailsFlyoutTabChanged',
+ payload: {
+ flyoutView: EndpointDetailsTabsTypes.activityLog,
+ },
+ });
+ };
- const fleetActionGenerator = new FleetActionGenerator(Math.random().toString());
+ const fleetActionGenerator = new FleetActionGenerator('seed');
const actionData = fleetActionGenerator.generate({
agents: [endpointList.hosts[0].metadata.agent.id],
});
@@ -265,6 +274,7 @@ describe('endpoint list middleware', () => {
it('should set ActivityLog state to loading', async () => {
dispatchUserChangedUrl();
+ dispatchFlyoutViewChange();
const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', {
validate(action) {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
index 4f96223e8b789..53b30aeb02bd5 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
@@ -35,6 +35,7 @@ import {
getActivityLogDataPaging,
getLastLoadedActivityLogData,
detailsData,
+ getEndpointDetailsFlyoutView,
} from './selectors';
import { AgentIdsPendingActions, EndpointState, PolicyIds } from '../types';
import {
@@ -48,6 +49,7 @@ import {
ENDPOINT_ACTION_LOG_ROUTE,
HOST_METADATA_GET_ROUTE,
HOST_METADATA_LIST_ROUTE,
+ BASE_POLICY_RESPONSE_ROUTE,
metadataCurrentIndexPattern,
} from '../../../../../common/endpoint/constants';
import { IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public';
@@ -61,6 +63,7 @@ import { AppAction } from '../../../../common/store/actions';
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
import { ServerReturnedEndpointPackageInfo } from './action';
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
+import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
type EndpointPageStore = ImmutableMiddlewareAPI;
@@ -339,6 +342,28 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(error.body ?? error),
});
}
-
- // call the policy response api
- try {
- const policyResponse = await coreStart.http.get(`/api/endpoint/policy_response`, {
- query: { agentId: selectedEndpoint },
- });
- dispatch({
- type: 'serverReturnedEndpointPolicyResponse',
- payload: policyResponse,
- });
- } catch (error) {
- dispatch({
- type: 'serverFailedToReturnEndpointPolicyResponse',
- payload: error,
- });
- }
}
// page activity log API
@@ -408,17 +417,24 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(updatedLogData),
});
- // TODO dispatch 'noNewLogData' if !activityLog.length
- // resets paging to previous state
+ if (!activityLog.data.length) {
+ dispatch({
+ type: 'endpointDetailsActivityLogUpdatePaging',
+ payload: {
+ disabled: true,
+ page: activityLog.page - 1,
+ pageSize: activityLog.pageSize,
+ },
+ });
+ }
} else {
dispatch({
type: 'endpointDetailsActivityLogChanged',
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
index 9460c27dfe705..44c63edd8e95c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
@@ -29,12 +29,23 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer {
+ const pagingOptions =
+ action.payload.type === 'LoadedResourceState'
+ ? {
+ ...state.endpointDetails.activityLog,
+ paging: {
+ ...state.endpointDetails.activityLog.paging,
+ page: action.payload.data.page,
+ pageSize: action.payload.data.pageSize,
+ },
+ }
+ : { ...state.endpointDetails.activityLog };
return {
...state!,
endpointDetails: {
...state.endpointDetails!,
activityLog: {
- ...state.endpointDetails.activityLog,
+ ...pagingOptions,
logData: action.payload,
},
},
@@ -138,7 +149,8 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
},
};
} else if (action.type === 'appRequestedEndpointActivityLog') {
- const pageData = {
+ const paging = {
+ disabled: state.endpointDetails.activityLog.paging.disabled,
page: action.payload.page,
pageSize: action.payload.pageSize,
};
@@ -148,10 +160,32 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
...state.endpointDetails!,
activityLog: {
...state.endpointDetails.activityLog,
- ...pageData,
+ paging,
},
},
};
+ } else if (action.type === 'endpointDetailsActivityLogUpdatePaging') {
+ const paging = {
+ ...action.payload,
+ };
+ return {
+ ...state,
+ endpointDetails: {
+ ...state.endpointDetails!,
+ activityLog: {
+ ...state.endpointDetails.activityLog,
+ paging,
+ },
+ },
+ };
+ } else if (action.type === 'endpointDetailsFlyoutTabChanged') {
+ return {
+ ...state,
+ endpointDetails: {
+ ...state.endpointDetails!,
+ flyoutView: action.payload.flyoutView,
+ },
+ };
} else if (action.type === 'endpointDetailsActivityLogChanged') {
return handleEndpointDetailsActivityLogChanged(state, action);
} else if (action.type === 'endpointPendingActionsStateChanged') {
@@ -255,8 +289,11 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
const activityLog = {
logData: createUninitialisedResourceState(),
- page: 1,
- pageSize: 50,
+ paging: {
+ disabled: false,
+ page: 1,
+ pageSize: 50,
+ },
};
// Reset `isolationRequestState` if needed
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
index d9be85377c81d..eeb54379e8e7d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
@@ -364,13 +364,14 @@ export const getIsolationRequestError: (
}
});
+export const getEndpointDetailsFlyoutView = (
+ state: Immutable
+): EndpointIndexUIQueryParams['show'] => state.endpointDetails.flyoutView;
+
export const getActivityLogDataPaging = (
state: Immutable
-): Immutable> => {
- return {
- page: state.endpointDetails.activityLog.page,
- pageSize: state.endpointDetails.activityLog.pageSize,
- };
+): Immutable => {
+ return state.endpointDetails.activityLog.paging;
};
export const getActivityLogData = (
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
index 59aa2bd15dd74..c985259588cb0 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
@@ -37,9 +37,13 @@ export interface EndpointState {
/** api error from retrieving host list */
error?: ServerApiError;
endpointDetails: {
+ flyoutView: EndpointIndexUIQueryParams['show'];
activityLog: {
- page: number;
- pageSize: number;
+ paging: {
+ disabled: boolean;
+ page: number;
+ pageSize: number;
+ };
logData: AsyncResourceState;
};
hostDetails: {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx
index 3e228be4565b1..aa1f56529657e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx
@@ -5,10 +5,15 @@
* 2.0.
*/
+import { useDispatch } from 'react-redux';
import React, { memo, useCallback, useMemo, useState } from 'react';
-import styled from 'styled-components';
-import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
+import { EuiTab, EuiTabs, EuiFlyoutBody, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui';
import { EndpointIndexUIQueryParams } from '../../../types';
+import { EndpointAction } from '../../../store/action';
+import { useEndpointSelector } from '../../hooks';
+import { getActivityLogDataPaging } from '../../../store/selectors';
+import { EndpointDetailsFlyoutHeader } from './flyout_header';
+
export enum EndpointDetailsTabsTypes {
overview = 'overview',
activityLog = 'activity_log',
@@ -24,29 +29,18 @@ interface EndpointDetailsTabs {
content: JSX.Element;
}
-const StyledEuiTabbedContent = styled(EuiTabbedContent)`
- overflow: hidden;
- padding-bottom: ${(props) => props.theme.eui.paddingSizes.xl};
-
- > [role='tabpanel'] {
- height: 100%;
- padding-right: 12px;
- overflow: hidden;
- overflow-y: auto;
- ::-webkit-scrollbar {
- -webkit-appearance: none;
- width: 4px;
- }
- ::-webkit-scrollbar-thumb {
- border-radius: 2px;
- background-color: rgba(0, 0, 0, 0.5);
- -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
- }
- }
-`;
-
export const EndpointDetailsFlyoutTabs = memo(
- ({ show, tabs }: { show: EndpointIndexUIQueryParams['show']; tabs: EndpointDetailsTabs[] }) => {
+ ({
+ hostname,
+ show,
+ tabs,
+ }: {
+ hostname?: string;
+ show: EndpointIndexUIQueryParams['show'];
+ tabs: EndpointDetailsTabs[];
+ }) => {
+ const dispatch = useDispatch<(action: EndpointAction) => void>();
+ const { pageSize } = useEndpointSelector(getActivityLogDataPaging);
const [selectedTabId, setSelectedTabId] = useState(() => {
return show === 'details'
? EndpointDetailsTabsTypes.overview
@@ -54,8 +48,33 @@ export const EndpointDetailsFlyoutTabs = memo(
});
const handleTabClick = useCallback(
- (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EndpointDetailsTabsId),
- [setSelectedTabId]
+ (tab: EuiTabbedContentTab) => {
+ dispatch({
+ type: 'endpointDetailsFlyoutTabChanged',
+ payload: {
+ flyoutView: tab.id as EndpointIndexUIQueryParams['show'],
+ },
+ });
+ if (tab.id === EndpointDetailsTabsTypes.activityLog) {
+ const paging = {
+ page: 1,
+ pageSize,
+ };
+ dispatch({
+ type: 'appRequestedEndpointActivityLog',
+ payload: paging,
+ });
+ dispatch({
+ type: 'endpointDetailsActivityLogUpdatePaging',
+ payload: {
+ disabled: false,
+ ...paging,
+ },
+ });
+ }
+ return setSelectedTabId(tab.id as EndpointDetailsTabsId);
+ },
+ [dispatch, pageSize, setSelectedTabId]
);
const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [
@@ -63,14 +82,27 @@ export const EndpointDetailsFlyoutTabs = memo(
selectedTabId,
]);
+ const renderTabs = tabs.map((tab) => (
+ handleTabClick(tab)}
+ isSelected={tab.id === selectedTabId}
+ key={tab.id}
+ data-test-subj={tab.id}
+ >
+ {tab.name}
+
+ ));
+
return (
-
+ <>
+
+
+ {renderTabs}
+
+
+ {selectedTab?.content}
+
+ >
);
}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx
new file mode 100644
index 0000000000000..f791c0d6adf17
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { EuiFlyoutHeader, EuiLoadingContent, EuiToolTip, EuiTitle } from '@elastic/eui';
+import { useEndpointSelector } from '../../hooks';
+import { detailsLoading } from '../../../store/selectors';
+
+export const EndpointDetailsFlyoutHeader = memo(
+ ({
+ hasBorder = false,
+ hostname,
+ children,
+ }: {
+ hasBorder?: boolean;
+ hostname?: string;
+ children?: React.ReactNode | React.ReactNodeArray;
+ }) => {
+ const hostDetailsLoading = useEndpointSelector(detailsLoading);
+
+ return (
+
+ {hostDetailsLoading ? (
+
+ ) : (
+
+
+
+ {hostname}
+
+
+
+ )}
+ {children}
+
+ );
+ }
+);
+
+EndpointDetailsFlyoutHeader.displayName = 'EndpointDetailsFlyoutHeader';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
index c431cd682d25b..4fe70039d1251 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
@@ -78,7 +78,7 @@ const useLogEntryUIProps = (
if (isSuccessful) {
return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful;
} else {
- return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful;
+ return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed;
}
} else {
if (isSuccessful) {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx
index 55479845bce0a..f1701054c4d5f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx
@@ -5,11 +5,19 @@
* 2.0.
*/
-import React, { memo, useCallback } from 'react';
+import React, { memo, useCallback, useEffect, useRef } from 'react';
+import styled from 'styled-components';
-import { EuiButton, EuiEmptyPrompt, EuiLoadingContent, EuiSpacer } from '@elastic/eui';
+import {
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingContent,
+ EuiEmptyPrompt,
+} from '@elastic/eui';
import { useDispatch } from 'react-redux';
import { LogEntry } from './components/log_entry';
+import * as i18 from '../translations';
import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types';
import { AsyncResourceState } from '../../../../state';
import { useEndpointSelector } from '../hooks';
@@ -19,54 +27,95 @@ import {
getActivityLogError,
getActivityLogIterableData,
getActivityLogRequestLoaded,
+ getLastLoadedActivityLogData,
getActivityLogRequestLoading,
} from '../../store/selectors';
+const LoadMoreTrigger = styled.div`
+ height: 6px;
+ width: 100%;
+`;
+
export const EndpointActivityLog = memo(
({ activityLog }: { activityLog: AsyncResourceState> }) => {
const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading);
const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded);
+ const activityLastLogData = useEndpointSelector(getLastLoadedActivityLogData);
const activityLogData = useEndpointSelector(getActivityLogIterableData);
+ const activityLogSize = activityLogData.length;
const activityLogError = useEndpointSelector(getActivityLogError);
- const dispatch = useDispatch<(a: EndpointAction) => void>();
- const { page, pageSize } = useEndpointSelector(getActivityLogDataPaging);
+ const dispatch = useDispatch<(action: EndpointAction) => void>();
+ const { page, pageSize, disabled: isPagingDisabled } = useEndpointSelector(
+ getActivityLogDataPaging
+ );
+
+ const loadMoreTrigger = useRef(null);
+ const getActivityLog = useCallback(
+ (entries: IntersectionObserverEntry[]) => {
+ const isTargetIntersecting = entries.some((entry) => entry.isIntersecting);
+ if (isTargetIntersecting && activityLogLoaded && !isPagingDisabled) {
+ dispatch({
+ type: 'appRequestedEndpointActivityLog',
+ payload: {
+ page: page + 1,
+ pageSize,
+ },
+ });
+ }
+ },
+ [activityLogLoaded, dispatch, isPagingDisabled, page, pageSize]
+ );
- const getActivityLog = useCallback(() => {
- dispatch({
- type: 'appRequestedEndpointActivityLog',
- payload: {
- page: page + 1,
- pageSize,
- },
- });
- }, [dispatch, page, pageSize]);
+ useEffect(() => {
+ const observer = new IntersectionObserver(getActivityLog);
+ const element = loadMoreTrigger.current;
+ if (element) {
+ observer.observe(element);
+ }
+ return () => {
+ observer.disconnect();
+ };
+ }, [getActivityLog]);
return (
<>
-
- {activityLogLoading || activityLogError ? (
- {'No logged actions'}}
- body={{'No actions have been logged for this endpoint.'}
}
- />
- ) : (
- <>
-
- {activityLogLoading ? (
-
- ) : (
- activityLogLoaded &&
- activityLogData.map((logEntry) => (
-
- ))
- )}
-
- {'show more'}
-
- >
- )}
+
+ {(activityLogLoaded && !activityLogSize) || activityLogError ? (
+
+ {i18.ACTIVITY_LOG.LogEntry.emptyState.title}}
+ body={{i18.ACTIVITY_LOG.LogEntry.emptyState.body}
}
+ data-test-subj="activityLogEmpty"
+ />
+
+ ) : (
+ <>
+
+ {activityLogLoaded &&
+ activityLogData.map((logEntry) => (
+
+ ))}
+ {activityLogLoading &&
+ activityLastLogData?.data.map((logEntry) => (
+
+ ))}
+
+
+ {activityLogLoading && }
+ {(!activityLogLoading || !isPagingDisabled) && (
+
+ )}
+ {isPagingDisabled && !activityLogLoading && (
+
+ {i18.ACTIVITY_LOG.LogEntry.endOfLog}
+
+ )}
+
+ >
+ )}
+
>
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
index d839bbfaae875..d3c91f6f18499 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx
@@ -20,7 +20,6 @@ export const dummyEndpointActivityLog = (
): AsyncResourceState> => ({
type: 'LoadedResourceState',
data: {
- total: 20,
page: 1,
pageSize: 50,
data: [
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
index 59e0c0e787a22..edfa410ee5237 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
@@ -5,21 +5,16 @@
* 2.0.
*/
+import { useDispatch } from 'react-redux';
import React, { useCallback, useEffect, useMemo, memo } from 'react';
-import styled from 'styled-components';
import {
EuiFlyout,
EuiFlyoutBody,
- EuiFlyoutHeader,
EuiFlyoutFooter,
EuiLoadingContent,
- EuiTitle,
EuiText,
EuiSpacer,
EuiEmptyPrompt,
- EuiToolTip,
- EuiFlexGroup,
- EuiFlexItem,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -30,7 +25,6 @@ import {
uiQueryParams,
detailsData,
detailsError,
- detailsLoading,
getActivityLogData,
showView,
policyResponseConfigurations,
@@ -59,23 +53,12 @@ import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpo
import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding';
import { getEndpointListPath } from '../../../../common/routing';
import { ActionsMenu } from './components/actions_menu';
-
-const DetailsFlyoutBody = styled(EuiFlyoutBody)`
- overflow-y: hidden;
- flex: 1;
-
- .euiFlyoutBody__overflow {
- overflow: hidden;
- mask-image: none;
- }
-
- .euiFlyoutBody__overflowContent {
- height: 100%;
- display: flex;
- }
-`;
+import { EndpointIndexUIQueryParams } from '../../types';
+import { EndpointAction } from '../../store/action';
+import { EndpointDetailsFlyoutHeader } from './components/flyout_header';
export const EndpointDetailsFlyout = memo(() => {
+ const dispatch = useDispatch<(action: EndpointAction) => void>();
const history = useHistory();
const toasts = useToasts();
const queryParams = useEndpointSelector(uiQueryParams);
@@ -86,13 +69,24 @@ export const EndpointDetailsFlyout = memo(() => {
const activityLog = useEndpointSelector(getActivityLogData);
const hostDetails = useEndpointSelector(detailsData);
- const hostDetailsLoading = useEndpointSelector(detailsLoading);
const hostDetailsError = useEndpointSelector(detailsError);
const policyInfo = useEndpointSelector(policyVersionInfo);
const hostStatus = useEndpointSelector(hostStatusInfo);
const show = useEndpointSelector(showView);
+ const setFlyoutView = useCallback(
+ (flyoutView: EndpointIndexUIQueryParams['show']) => {
+ dispatch({
+ type: 'endpointDetailsFlyoutTabChanged',
+ payload: {
+ flyoutView,
+ },
+ });
+ },
+ [dispatch]
+ );
+
const ContentLoadingMarkup = useMemo(
() => (
<>
@@ -133,9 +127,11 @@ export const EndpointDetailsFlyout = memo(() => {
...urlSearchParams,
})
);
- }, [history, queryParamsWithoutSelectedEndpoint]);
+ setFlyoutView(undefined);
+ }, [setFlyoutView, history, queryParamsWithoutSelectedEndpoint]);
useEffect(() => {
+ setFlyoutView(show);
if (hostDetailsError !== undefined) {
toasts.addDanger({
title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', {
@@ -146,7 +142,10 @@ export const EndpointDetailsFlyout = memo(() => {
}),
});
}
- }, [hostDetailsError, toasts]);
+ return () => {
+ setFlyoutView(undefined);
+ };
+ }, [hostDetailsError, setFlyoutView, show, toasts]);
return (
{
data-test-subj="endpointDetailsFlyout"
size="m"
paddingSize="l"
+ ownFocus={false}
>
-
- {hostDetailsLoading ? (
-
- ) : (
-
-
-
- {hostDetails?.host?.hostname}
-
-
-
- )}
-
+ {(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && (
+
+ )}
{hostDetails === undefined ? (
@@ -179,13 +166,11 @@ export const EndpointDetailsFlyout = memo(() => {
) : (
<>
{(show === 'details' || show === 'activity_log') && (
-
-
-
-
-
-
-
+
)}
{show === 'policy_response' && }
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 6aab9336c21a4..4869ce84fad2c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -17,6 +17,7 @@ import {
} from '../store/mock_endpoint_result_list';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import {
+ ActivityLog,
HostInfo,
HostPolicyResponse,
HostPolicyResponseActionStatus,
@@ -32,12 +33,15 @@ import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kib
import { hostIsolationHttpMocks } from '../../../../common/lib/endpoint_isolation/mocks';
import { fireEvent } from '@testing-library/dom';
import {
+ createFailedResourceState,
+ createLoadedResourceState,
isFailedResourceState,
isLoadedResourceState,
isUninitialisedResourceState,
} from '../../../state';
import { getCurrentIsolationRequestState } from '../store/selectors';
import { licenseService } from '../../../../common/hooks/use_license';
+import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator';
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
// but sure enough it needs to be inline in this one file
@@ -625,6 +629,30 @@ describe('when on the endpoint list page', () => {
});
};
+ const dispatchEndpointDetailsActivityLogChanged = (
+ dataState: 'failed' | 'success',
+ data: ActivityLog
+ ) => {
+ reactTestingLibrary.act(() => {
+ const getPayload = () => {
+ switch (dataState) {
+ case 'failed':
+ return createFailedResourceState({
+ statusCode: 500,
+ error: 'Internal Server Error',
+ message: 'An internal server error occurred.',
+ });
+ case 'success':
+ return createLoadedResourceState(data);
+ }
+ };
+ store.dispatch({
+ type: 'endpointDetailsActivityLogChanged',
+ payload: getPayload(),
+ });
+ });
+ };
+
beforeEach(async () => {
mockEndpointListApi();
@@ -746,6 +774,120 @@ describe('when on the endpoint list page', () => {
expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull();
});
+ describe('when showing Activity Log panel', () => {
+ let renderResult: ReturnType;
+ const agentId = 'some_agent_id';
+
+ let getMockData: () => ActivityLog;
+ beforeEach(async () => {
+ window.IntersectionObserver = jest.fn(() => ({
+ root: null,
+ rootMargin: '',
+ thresholds: [],
+ takeRecords: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+ }));
+
+ const fleetActionGenerator = new FleetActionGenerator('seed');
+ const responseData = fleetActionGenerator.generateResponse({
+ agent_id: agentId,
+ });
+ const actionData = fleetActionGenerator.generate({
+ agents: [agentId],
+ });
+ getMockData = () => ({
+ page: 1,
+ pageSize: 50,
+ data: [
+ {
+ type: 'response',
+ item: {
+ id: 'some_id_0',
+ data: responseData,
+ },
+ },
+ {
+ type: 'action',
+ item: {
+ id: 'some_id_1',
+ data: actionData,
+ },
+ },
+ ],
+ });
+
+ renderResult = render();
+ await reactTestingLibrary.act(async () => {
+ await middlewareSpy.waitForAction('serverReturnedEndpointList');
+ });
+ const hostNameLinks = await renderResult.getAllByTestId('hostnameCellLink');
+ reactTestingLibrary.fireEvent.click(hostNameLinks[0]);
+ });
+
+ afterEach(reactTestingLibrary.cleanup);
+
+ it('should show the endpoint details flyout', async () => {
+ const activityLogTab = await renderResult.findByTestId('activity_log');
+ reactTestingLibrary.act(() => {
+ reactTestingLibrary.fireEvent.click(activityLogTab);
+ });
+ await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
+ reactTestingLibrary.act(() => {
+ dispatchEndpointDetailsActivityLogChanged('success', getMockData());
+ });
+ const endpointDetailsFlyout = await renderResult.queryByTestId('endpointDetailsFlyoutBody');
+ expect(endpointDetailsFlyout).not.toBeNull();
+ });
+
+ it('should display log accurately', async () => {
+ const activityLogTab = await renderResult.findByTestId('activity_log');
+ reactTestingLibrary.act(() => {
+ reactTestingLibrary.fireEvent.click(activityLogTab);
+ });
+ await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
+ reactTestingLibrary.act(() => {
+ dispatchEndpointDetailsActivityLogChanged('success', getMockData());
+ });
+ const logEntries = await renderResult.queryAllByTestId('timelineEntry');
+ expect(logEntries.length).toEqual(2);
+ expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null);
+ expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null);
+ });
+
+ it('should display empty state when API call has failed', async () => {
+ const activityLogTab = await renderResult.findByTestId('activity_log');
+ reactTestingLibrary.act(() => {
+ reactTestingLibrary.fireEvent.click(activityLogTab);
+ });
+ await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
+ reactTestingLibrary.act(() => {
+ dispatchEndpointDetailsActivityLogChanged('failed', getMockData());
+ });
+ const emptyState = await renderResult.queryByTestId('activityLogEmpty');
+ expect(emptyState).not.toBe(null);
+ });
+
+ it('should display empty state when no log data', async () => {
+ const activityLogTab = await renderResult.findByTestId('activity_log');
+ reactTestingLibrary.act(() => {
+ reactTestingLibrary.fireEvent.click(activityLogTab);
+ });
+ await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
+ reactTestingLibrary.act(() => {
+ dispatchEndpointDetailsActivityLogChanged('success', {
+ page: 1,
+ pageSize: 50,
+ data: [],
+ });
+ });
+
+ const emptyState = await renderResult.queryByTestId('activityLogEmpty');
+ expect(emptyState).not.toBe(null);
+ });
+ });
+
describe('when showing host Policy Response panel', () => {
let renderResult: ReturnType;
beforeEach(async () => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
index 18a5bd1e5130a..89ffd2d23807e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
@@ -16,6 +16,26 @@ export const ACTIVITY_LOG = {
defaultMessage: 'Activity Log',
}),
LogEntry: {
+ endOfLog: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog',
+ {
+ defaultMessage: 'Nothing more to show',
+ }
+ ),
+ emptyState: {
+ title: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.title',
+ {
+ defaultMessage: 'No logged actions',
+ }
+ ),
+ body: i18n.translate(
+ 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.body',
+ {
+ defaultMessage: 'No actions have been logged for this endpoint.',
+ }
+ ),
+ },
action: {
isolatedAction: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.isolated',
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
index 204c3a86ce3e6..e9cdd16554f33 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
@@ -42,7 +42,7 @@ import { useFormatUrl } from '../../../../common/components/link_to';
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import { MANAGEMENT_APP_ID } from '../../../common/constants';
import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types';
-import { WrapperPage } from '../../../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper';
import { HeaderPage } from '../../../../common/components/header_page';
import { PolicyDetailsForm } from './policy_details_form';
@@ -51,7 +51,7 @@ const PolicyDetailsHeader = styled.div`
padding: ${(props) => props.theme.eui.paddingSizes.xl} 0;
background-color: #fafbfd;
border-bottom: 1px solid #d3dae6;
- .siemHeaderPage {
+ .securitySolutionHeaderPage {
max-width: ${maxFormWidth};
margin: 0 auto;
}
@@ -159,7 +159,7 @@ export const PolicyDetails = React.memo(() => {
// Else, if we have an error, then show error on the page.
if (!policyItem) {
return (
-
+
{isPolicyLoading ? (
) : policyApiError ? (
@@ -168,7 +168,7 @@ export const PolicyDetails = React.memo(() => {
) : null}
-
+
);
}
@@ -190,7 +190,7 @@ export const PolicyDetails = React.memo(() => {
onConfirm={handleSaveConfirmation}
/>
)}
- {
-
+
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
index e984ea5bb1711..7b3ae2e2b3b27 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
@@ -34,15 +34,8 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = `
-
-
- No items found
-
-
+
+ No items found
@@ -427,7 +420,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
class="body-content undefined"
>
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx
index 82b5b8a3e7b3d..3087dbe4ad6ed 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx
@@ -20,7 +20,9 @@ export interface EmbeddableProps {
export const Embeddable = React.memo(({ children }) => (
- {children}
+
+ {children}
+
));
Embeddable.displayName = 'Embeddable';
diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx
index a3fd32008062c..63971ae508d5c 100644
--- a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { Ip } from '.';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
index 7ec18c078c73d..a811f5c92c37a 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
@@ -25,6 +25,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { NetworkDnsTable } from '.';
import { mockData } from './mock';
+jest.mock('../../../common/lib/kibana');
+
describe('NetworkTopNFlow Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
index f7f75d9f0a365..f05372c76b36f 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
@@ -25,6 +25,7 @@ import { networkModel } from '../../store';
import { NetworkHttpTable } from '.';
import { mockData } from './mock';
+jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/components/link_to');
describe('NetworkHttp Table Component', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
index 1501f56882290..a0727fad65f18 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
@@ -27,6 +27,8 @@ import { networkModel } from '../../store';
import { NetworkTopCountriesTable } from '.';
import { mockData } from './mock';
+jest.mock('../../../common/lib/kibana');
+
describe('NetworkTopCountries Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
index cd8c8c6543299..e2b9447b58806 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
@@ -25,6 +25,7 @@ import { NetworkTopNFlowTable } from '.';
import { mockData } from './mock';
import { FlowTargetSourceDest } from '../../../../common/search_strategy';
+jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/components/link_to');
describe('NetworkTopNFlow Table Component', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx
index ef1039bfc92e3..dd7ad20d2384a 100644
--- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx
@@ -15,6 +15,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { Port } from '.';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx
index 01065ad5bf15f..b59eb25cbfe25 100644
--- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx
@@ -49,6 +49,8 @@ import {
NETWORK_TRANSPORT_FIELD_NAME,
} from './field_names';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx
index f767e793c8f21..91f7ea3d7ac7a 100644
--- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx
@@ -38,6 +38,8 @@ import {
SOURCE_GEO_REGION_NAME_FIELD_NAME,
} from './geo_fields';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../../../common/components/link_to');
describe('SourceDestinationIp', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
index 4b6c31f5b6176..8f2c7a098a045 100644
--- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
@@ -24,6 +24,8 @@ import { networkModel } from '../../store';
import { TlsTable } from '.';
import { mockTlsData } from './mock';
+jest.mock('../../../common/lib/kibana');
+
describe('Tls Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
index 4b613e79a1d1a..69027ad9bd9f8 100644
--- a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
@@ -26,6 +26,8 @@ import { UsersTable } from '.';
import { mockUsersData } from './mock';
import { FlowTarget } from '../../../../common/search_strategy';
+jest.mock('../../../common/lib/kibana');
+
describe('Users Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx
index 4cccb536c08bb..02be5f78261c1 100644
--- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx
@@ -28,7 +28,7 @@ import { manageQuery } from '../../../common/components/page/manage_query';
import { FlowTargetSelectConnected } from '../../components/flow_target_select_connected';
import { IpOverview } from '../../components/details';
import { SiemSearchBar } from '../../../common/components/search_bar';
-import { WrapperPage } from '../../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
import { useNetworkDetails } from '../../containers/details';
import { useKibana } from '../../../common/lib/kibana';
import { decodeIpv6 } from '../../../common/lib/helpers';
@@ -128,7 +128,7 @@ const NetworkDetailsComponent: React.FC = () => {
-
+
{
hideHistogramIfEmpty={true}
AnomaliesTableComponent={AnomaliesNetworkTable}
/>
-
+
>
) : (
-
+
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx
index 2bcc72d932a9b..13c04a5e5ec5b 100644
--- a/x-pack/plugins/security_solution/public/network/pages/network.tsx
+++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx
@@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
+import { isTab } from '../../../../timelines/public';
import { esQuery } from '../../../../../../src/plugins/data/public';
import { SecurityPageName } from '../../app/types';
import { UpdateDateRange } from '../../common/components/charts/common';
@@ -19,11 +20,11 @@ import { EmbeddedMap } from '../components/embeddables/embedded_map';
import { FiltersGlobal } from '../../common/components/filters_global';
import { HeaderPage } from '../../common/components/header_page';
import { LastEventTime } from '../../common/components/last_event_time';
-import { SiemNavigation } from '../../common/components/navigation';
+import { SecuritySolutionTabNavigation } from '../../common/components/navigation';
import { NetworkKpiComponent } from '../components/kpi_network';
import { SiemSearchBar } from '../../common/components/search_bar';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useGlobalFullScreen } from '../../common/containers/use_full_screen';
import { useGlobalTime } from '../../common/containers/use_global_time';
import { LastEventIndexKey } from '../../../common/search_strategy';
@@ -46,7 +47,6 @@ import {
showGlobalFilters,
} from '../../timelines/components/timeline/helpers';
import { timelineSelectors } from '../../timelines/store/timeline';
-import { isTab } from '../../common/components/accessibility/helpers';
import { TimelineId } from '../../../common/types/timeline';
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../common/containers/sourcerer';
@@ -155,10 +155,9 @@ const NetworkComponent = React.memo(
-
+
(
-
+
@@ -217,13 +216,13 @@ const NetworkComponent = React.memo(
) : (
)}
-
+
) : (
-
+
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx
index b43d5af029ec4..45898427ee60b 100644
--- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx
@@ -15,6 +15,8 @@ import { TestProviders } from '../../../../common/mock';
import { EndpointOverview } from './index';
import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy/security_solution/hosts';
+jest.mock('../../../../common/lib/kibana');
+
describe('EndpointOverview Component', () => {
test('it renders with endpoint data', () => {
const endpointData = {
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx
index 70f44a0008cbc..f11b849f5df6b 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx
@@ -115,7 +115,7 @@ const OverviewHostComponent: React.FC = ({
return (
-
+
<>{hostPageButton}>
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx
index 107a47f6cc132..39fb6ff08ee53 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx
@@ -120,7 +120,7 @@ const OverviewNetworkComponent: React.FC = ({
return (
-
+
<>
{networkPageButton}
diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx
index 4270d8ec164b3..2cf998e5e133a 100644
--- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx
+++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx
@@ -12,7 +12,7 @@ import styled from 'styled-components';
import { AlertsByCategory } from '../components/alerts_by_category';
import { FiltersGlobal } from '../../common/components/filters_global';
import { SiemSearchBar } from '../../common/components/search_bar';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useGlobalTime } from '../../common/containers/use_global_time';
import { useFetchIndex } from '../../common/containers/source';
@@ -37,6 +37,10 @@ const SidebarFlexItem = styled(EuiFlexItem)`
margin-right: 24px;
`;
+const StyledSecuritySolutionPageWrapper = styled(SecuritySolutionPageWrapper)`
+ overflow-x: auto;
+`;
+
const OverviewComponent = () => {
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
@@ -73,7 +77,7 @@ const OverviewComponent = () => {
-
+
{!dismissMessage && !metadataIndexExists && isIngestEnabled && (
<>
@@ -139,7 +143,7 @@ const OverviewComponent = () => {
-
+
>
) : (
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 781ed8ffdaa54..32e6748f38141 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -6,8 +6,10 @@
*/
import { i18n } from '@kbn/i18n';
+import reduceReducers from 'reduce-reducers';
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { pluck } from 'rxjs/operators';
+import { AnyAction, Reducer } from 'redux';
import {
PluginSetup,
PluginStart,
@@ -59,7 +61,7 @@ import {
DETECTION_ENGINE,
CASE,
ADMINISTRATION,
-} from './app/home/translations';
+} from './app/translations';
import {
IndexFieldsStrategyRequest,
IndexFieldsStrategyResponse,
@@ -72,6 +74,7 @@ import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/vi
import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension';
import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension';
import { parseExperimentalConfigValue } from '../common/experimental_features';
+import type { TimelineState } from '../../timelines/public';
export class Plugin implements IPlugin {
private kibanaVersion: string;
@@ -471,7 +474,7 @@ export class Plugin implements IPlugin(
{ indices: defaultIndicesName, onlyCheckIfIndicesExist: true },
{
- strategy: 'securitySolutionIndexFields',
+ strategy: 'indexFields',
}
)
.toPromise(),
@@ -500,7 +503,6 @@ export class Plugin implements IPlugin;
+
this._store = createStore(
createInitialState(
{
@@ -531,13 +540,17 @@ export class Plugin implements IPlugin
-
+
-
+
+
) : event ? (
@@ -71,7 +71,7 @@ export const EventDetail = memo(function EventDetail({
eventType={eventType}
/>
) : (
-
+
);
@@ -105,7 +105,7 @@ const EventDetailContents = memo(function ({
const nodeName = processEvent ? eventModel.processNameSafeVersion(processEvent) : null;
return (
-
+
selectors.nodeDataStatus(state)(nodeID));
return nodeStatus === 'loading' ? (
-
+
) : processEvent ? (
-
+
) : (
-
+
);
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx
index 951d2ff9fdc43..e7cd37506134f 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx
@@ -30,13 +30,13 @@ export function NodeEvents({ nodeID }: { nodeID: string }) {
if (processEvent === undefined || nodeStats === undefined) {
return (
-
+
);
} else {
return (
-
+
{isLoading ? (
-
+
) : (
-
+
{hasError || !node ? (
{
const showWarning = children === true || ancestors === true || generations === true;
const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []);
return (
-
+
{showWarning && }
diff --git a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx
index 851e8a2fcf10e..44522441e6e99 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx
@@ -95,6 +95,11 @@ export const StyledMapContainer = styled.div<{ backgroundColor: string }>`
justify-content: center;
flex-grow: 1;
}
+ /**
+ * Set to force base-height necessary for resolver to show up in timeline.
+ * Was previously set in events_viewer.tsx, but more appropriate here
+ */
+ min-height: 652px;
/**
* The placeholder components use absolute positioning.
*/
diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx
index 7a38c873450ca..4ebb804eab8a4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { CertificateFingerprint } from '.';
+jest.mock('../../../common/lib/kibana');
+
describe('CertificateFingerprint', () => {
const mount = useMountAppended();
test('renders the expected label', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx
index 4c90d3738a198..ea8317346cd99 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { Duration } from '.';
+jest.mock('../../../common/lib/kibana');
+
describe('Duration', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx
index 5becf7ea8bc6b..e2194156ecf4d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx
@@ -29,6 +29,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { AutonomousSystem, FlowTarget } from '../../../../common/search_strategy';
import { HostEcs } from '../../../../common/ecs/host';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
index 77a8d0082bf23..da2ff248d9a5d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
@@ -14,7 +14,7 @@ import {
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
-} from '../../../common/components/accessibility/helpers';
+} from '../../../../../timelines/public';
import { BrowserFields } from '../../../common/containers/source';
import { getCategoryColumns } from './category_columns';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx
index c3c55206f8d53..c95463dea5b27 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx
@@ -17,6 +17,9 @@ import { TestProviders } from '../../../common/mock';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
import * as i18n from './translations';
+
+jest.mock('../../../common/lib/kibana');
+
describe('Category', () => {
const timelineId = 'test';
const selectedCategoryId = 'client';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
index 636ebf022cffb..deafda95ceab2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
@@ -9,13 +9,13 @@ import { EuiInMemoryTable } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef } from 'react';
import styled from 'styled-components';
-
import {
arrayIndexToAriaIndex,
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
-} from '../../../common/components/accessibility/helpers';
+} from '../../../../../timelines/public';
+
import { BrowserFields } from '../../../common/containers/source';
import { OnUpdateColumns } from '../timeline/events';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
index 15164cd151574..528791328fdb9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
@@ -18,6 +18,7 @@ import {
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { BrowserFields } from '../../../common/containers/source';
import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers';
import { CountBadge } from '../../../common/components/page';
@@ -29,7 +30,7 @@ import {
VIEW_ALL_BUTTON_CLASS_NAME,
} from './helpers';
import * as i18n from './translations';
-import { useManageTimeline } from '../manage_timeline';
+import { timelineSelectors } from '../../store/timeline';
const CategoryName = styled.span<{ bold: boolean }>`
.euiText {
@@ -67,11 +68,10 @@ interface ViewAllButtonProps {
export const ViewAllButton = React.memo(
({ categoryId, browserFields, onUpdateColumns, timelineId }) => {
- const { getManageTimelineById } = useManageTimeline();
- const { isLoading } = useMemo(() => getManageTimelineById(timelineId) ?? { isLoading: false }, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, timelineId ?? '')
+ );
const handleClick = useCallback(() => {
onUpdateColumns(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
index 70cc535cb59a9..6af4b5c5c312e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
@@ -9,6 +9,7 @@ import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
+import { TestProviders } from '../../../common/mock';
import { CategoryTitle } from './category_title';
import { getFieldCount } from './helpers';
@@ -19,12 +20,14 @@ describe('CategoryTitle', () => {
test('it renders the category id as the value of the title', () => {
const categoryId = 'client';
const wrapper = mount(
-
+
+
+
);
expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual(
@@ -35,12 +38,14 @@ describe('CategoryTitle', () => {
test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => {
const validCategoryId = 'client';
const wrapper = mount(
-
+
+
+
);
expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual(
@@ -51,12 +56,14 @@ describe('CategoryTitle', () => {
test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => {
const invalidCategoryId = 'this.is.not.happening';
const wrapper = mount(
-
+
+
+
);
expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
index c4f76c639c7c1..0496b9d7c8886 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
@@ -19,13 +19,8 @@ import { noop } from 'lodash/fp';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
-import {
- isEscape,
- isTab,
- stopPropagationAndPreventDefault,
-} from '../../../common/components/accessibility/helpers';
+import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../timelines/public';
import { BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { CategoriesPane } from './categories_pane';
import { FieldsPane } from './fields_pane';
import { Header } from './header';
@@ -42,6 +37,7 @@ import { FieldBrowserProps, OnHideFieldBrowser } from './types';
import { timelineActions } from '../../store/timeline';
import * as i18n from './translations';
+import { ColumnHeaderOptions } from '../../../../common';
const FieldsBrowserContainer = styled.div<{ width: number }>`
background-color: ${({ theme }) => theme.eui.euiColorLightestShade};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
index 07911541bb2fe..e40807dc85dc7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
@@ -12,7 +12,6 @@ import { waitFor } from '@testing-library/react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import '../../../common/mock/match_media';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants';
@@ -20,6 +19,9 @@ import { Category } from './category';
import { getFieldColumns, getFieldItems } from './field_items';
import { FIELDS_PANE_WIDTH } from './helpers';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
+import { ColumnHeaderOptions } from '../../../../common';
+
+jest.mock('../../../common/lib/kibana');
const selectedCategoryId = 'base';
const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx
index a2db284e51790..89a91ee6da305 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx
@@ -18,14 +18,12 @@ import React, { useCallback, useRef, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { BrowserField, BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
-import { useDraggableKeyboardWrapper } from '../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper';
import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper';
import {
DRAG_TYPE_FIELD,
- DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
getDraggableFieldId,
getDroppableId,
} from '../../../common/components/drag_and_drop/helpers';
@@ -43,6 +41,8 @@ import { TruncatableText } from '../../../common/components/truncatable_text';
import { FieldName } from './field_name';
import * as i18n from './translations';
import { getAlertColumnHeader } from './helpers';
+import { ColumnHeaderOptions } from '../../../../common';
+import { useKibana } from '../../../common/lib/kibana';
const TypeIcon = styled(EuiIcon)`
margin: 0 4px;
@@ -92,6 +92,7 @@ const DraggableFieldsBrowserFieldComponent = ({
const keyboardHandlerRef = useRef(null);
const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false);
+ const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
@@ -115,7 +116,7 @@ const DraggableFieldsBrowserFieldComponent = ({
setHoverActionsOwnFocus(true);
}, [setHoverActionsOwnFocus]);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId: getDraggableFieldId({
contextId: `field-browser-field-items-field-draggable-${timelineId}-${categoryId}-${fieldName}`,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx
index 493f2e44263e3..5014a198e8bd5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx
@@ -15,6 +15,8 @@ import { getColumnsWithTimestamp } from '../../../common/components/event_detail
import { FieldName } from './field_name';
+jest.mock('../../../common/lib/kibana');
+
const categoryId = 'base';
const timestampFieldId = '@timestamp';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx
index 09bd18ef62fb1..2e76e43227506 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx
@@ -9,13 +9,13 @@ import { EuiHighlight, EuiText } from '@elastic/eui';
import React, { useCallback, useState, useMemo, useRef } from 'react';
import styled from 'styled-components';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { OnUpdateColumns } from '../timeline/events';
import { WithHoverActions } from '../../../common/components/with_hover_actions';
import {
DraggableWrapperHoverContent,
useGetTimelineId,
} from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content';
+import { ColumnHeaderOptions } from '../../../../common';
/**
* The name of a (draggable) field
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
index 3f1b0300ad70d..6d17f148aa1dc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
@@ -15,6 +15,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { FIELDS_PANE_WIDTH } from './helpers';
import { FieldsPane } from './fields_pane';
+jest.mock('../../../common/lib/kibana');
+
const timelineId = 'test';
describe('FieldsPane', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
index 15df232a1a454..dfb4edad17414 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
@@ -11,7 +11,6 @@ import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { timelineActions } from '../../../timelines/store/timeline';
import { OnUpdateColumns } from '../timeline/events';
import { Category } from './category';
@@ -20,6 +19,7 @@ import { getFieldItems } from './field_items';
import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers';
import * as i18n from './translations';
+import { ColumnHeaderOptions } from '../../../../common';
const NoFieldsPanel = styled.div`
background-color: ${(props) => props.theme.eui.euiColorLightestShade};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
index aa53b1922f3a3..89b361e86422e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
@@ -9,7 +9,6 @@ import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
-import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
import { Header } from './header';
const timelineId = 'test';
@@ -72,7 +71,7 @@ describe('Header', () => {
wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click');
- expect(onUpdateColumns).toBeCalledWith(defaultHeaders);
+ expect(onUpdateColumns).toBeCalled();
});
test('it invokes onOutsideClick when the user clicks the Reset Fields button', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
index 120a82a4046e3..b52c6cd672ac7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
@@ -13,10 +13,12 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { BrowserFields } from '../../../common/containers/source';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
+import { timelineSelectors } from '../../store/timeline';
import { OnUpdateColumns } from '../timeline/events';
import {
@@ -27,7 +29,6 @@ import {
} from './helpers';
import * as i18n from './translations';
-import { useManageTimeline } from '../manage_timeline';
const CountsFlexGroup = styled(EuiFlexGroup)`
margin-top: 5px;
@@ -101,13 +102,13 @@ const TitleRow = React.memo<{
onOutsideClick: () => void;
onUpdateColumns: OnUpdateColumns;
}>(({ id, onOutsideClick, onUpdateColumns }) => {
- const { getManageTimelineById } = useManageTimeline();
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const handleResetColumns = useCallback(() => {
- const timeline = getManageTimelineById(id);
- onUpdateColumns(timeline.defaultModel.columns);
+ onUpdateColumns(defaultColumns);
onOutsideClick();
- }, [id, onUpdateColumns, onOutsideClick, getManageTimelineById]);
+ }, [onUpdateColumns, onOutsideClick, defaultColumns]);
return (
{
const timelineId = 'test';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
index 4d912f73c7ef2..ea71a8860ab01 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
+import { ColumnHeaderOptions } from '../../../../common';
import { BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
export type OnFieldSelected = (fieldId: string) => void;
export type OnHideFieldBrowser = () => void;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx
index 7b43fb9c7194c..32d36006fffd5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx
@@ -6,60 +6,19 @@
*/
import { EuiPanel } from '@elastic/eui';
-import { rgba } from 'polished';
import React from 'react';
import styled from 'styled-components';
-import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers';
import { DataProvider } from '../../timeline/data_providers/data_provider';
import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers';
import { DataProviders } from '../../timeline/data_providers';
-import { FLYOUT_BUTTON_BAR_CLASS_NAME, FLYOUT_BUTTON_CLASS_NAME } from '../../timeline/helpers';
+import { FLYOUT_BUTTON_BAR_CLASS_NAME } from '../../timeline/helpers';
import { FlyoutHeaderPanel } from '../header';
import { TimelineTabs } from '../../../../../common/types/timeline';
export const getBadgeCount = (dataProviders: DataProvider[]): number =>
flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0);
-const SHOW_HIDE_GLOBAL_TRANSLATE_Y = 50; // px
-const SHOW_HIDE_TIMELINE_TRANSLATE_Y = 0; // px
-
-const Container = styled.div.attrs<{ $isGlobal: boolean }>(({ $isGlobal = true }) => ({
- style: {
- transform: $isGlobal
- ? `translateY(calc(100% - ${SHOW_HIDE_GLOBAL_TRANSLATE_Y}px))`
- : `translateY(calc(100% - ${SHOW_HIDE_TIMELINE_TRANSLATE_Y}px))`,
- },
-}))<{ $isGlobal: boolean }>`
- position: fixed;
- left: 0;
- bottom: 0;
- user-select: none;
- width: 100%;
- z-index: ${({ theme }) => theme.eui.euiZLevel8 + 1};
-
- .${IS_DRAGGING_CLASS_NAME} & {
- transform: none !important;
- }
-
- .${FLYOUT_BUTTON_CLASS_NAME} {
- background: ${({ theme }) => rgba(theme.eui.euiPageBackgroundColor, 1)};
- border-radius: 4px 4px 0 0;
- box-shadow: none;
- height: 46px;
- }
-
- .${IS_DRAGGING_CLASS_NAME} & .${FLYOUT_BUTTON_CLASS_NAME} {
- color: ${({ theme }) => theme.eui.euiColorSuccess};
- background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important;
- border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess};
- border-bottom: none;
- text-decoration: none;
- }
-`;
-
-Container.displayName = 'Container';
-
const DataProvidersPanel = styled(EuiPanel)`
border-radius: 0;
padding: 0 4px 0 4px;
@@ -76,18 +35,14 @@ interface FlyoutBottomBarProps {
export const FlyoutBottomBar = React.memo(
({ activeTab, showDataproviders, timelineId }) => {
return (
-
+
{showDataproviders && }
{(showDataproviders || (!showDataproviders && activeTab !== TimelineTabs.query)) && (
)}
-
+
);
}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx
index ec46985450d89..ad1d126e3c853 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx
@@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n';
import { EuiFocusTrap, EuiOutsideClickDetector } from '@elastic/eui';
import React, { useEffect, useMemo, useCallback, useState, useRef } from 'react';
import { useDispatch } from 'react-redux';
-import styled from 'styled-components';
import { AppLeaveHandler } from '../../../../../../../src/core/public';
import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline';
@@ -19,12 +18,6 @@ import { FlyoutBottomBar } from './bottom_bar';
import { Pane } from './pane';
import { getTimelineShowStatusByIdSelector } from './selectors';
-const Visible = styled.div<{ show?: boolean }>`
- visibility: ${({ show }) => (show ? 'visible' : 'hidden')};
-`;
-
-Visible.displayName = 'Visible';
-
interface OwnProps {
timelineId: TimelineId;
onAppLeave: (handler: AppLeaveHandler) => void;
@@ -124,9 +117,7 @@ const FlyoutComponent: React.FC = ({ timelineId, onAppLeave }) => {
<>
-
-
-
+
>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx
index 459706de36569..35a13aba471fd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx
@@ -20,6 +20,7 @@ import { focusActiveTimelineButton } from '../../timeline/helpers';
interface FlyoutPaneComponentProps {
timelineId: TimelineId;
+ visible?: boolean;
}
const EuiFlyoutContainer = styled.div`
@@ -31,7 +32,10 @@ const EuiFlyoutContainer = styled.div`
}
`;
-const FlyoutPaneComponent: React.FC = ({ timelineId }) => {
+const FlyoutPaneComponent: React.FC = ({
+ timelineId,
+ visible = true,
+}) => {
const dispatch = useDispatch();
const handleClose = useCallback(() => {
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
@@ -39,7 +43,10 @@ const FlyoutPaneComponent: React.FC = ({ timelineId })
}, [dispatch, timelineId]);
return (
-
+
= ({ timelineId })
hideCloseButton={true}
onClose={handleClose}
size="l"
+ ownFocus={false}
+ style={{ visibility: visible ? 'visible' : 'hidden' }}
>
{
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx
deleted file mode 100644
index ed299c3a4ef1a..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { renderHook, act } from '@testing-library/react-hooks';
-import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './';
-import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager';
-import { coreMock } from '../../../../../../../src/core/public/mocks';
-
-const isStringifiedComparisonEqual = (a: {}, b: {}): boolean =>
- JSON.stringify(a) === JSON.stringify(b);
-
-describe('useTimelineManager', () => {
- const setupMock = coreMock.createSetup();
- const testId = 'coolness';
- const timelineDefaults = getTimelineDefaults(testId);
- const mockFilterManager = new FilterManager(setupMock.uiSettings);
-
- beforeEach(() => {
- jest.clearAllMocks();
- jest.restoreAllMocks();
- });
-
- it('initializes an undefined timeline', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- const uninitializedTimeline = result.current.getManageTimelineById(testId);
- expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy();
- });
- });
- // TO DO sourcerer
- // it('getIndexToAddById', async () => {
- // await act(async () => {
- // const { result, waitForNextUpdate } = renderHook(() =>
- // useTimelineManager()
- // );
- // await waitForNextUpdate();
- // const data = result.current.getIndexToAddById(testId);
- // expect(data).toEqual(timelineDefaults.indexToAdd);
- // });
- // });
- //
- // it('setIndexToAdd', async () => {
- // await act(async () => {
- // const indexToAddArgs = { id: testId, indexToAdd: ['example'] };
- // const { result, waitForNextUpdate } = renderHook(() =>
- // useTimelineManager()
- // );
- // await waitForNextUpdate();
- // result.current.initializeTimeline({
- // id: testId,
- // });
- // result.current.setIndexToAdd(indexToAddArgs);
- // const data = result.current.getIndexToAddById(testId);
- // expect(data).toEqual(indexToAddArgs.indexToAdd);
- // });
- // });
-
- it('setIsTimelineLoading', async () => {
- await act(async () => {
- const isLoadingArgs = { id: testId, isLoading: true };
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- result.current.initializeTimeline({
- id: testId,
- });
- let timeline = result.current.getManageTimelineById(testId);
- expect(timeline.isLoading).toBeFalsy();
- result.current.setIsTimelineLoading(isLoadingArgs);
- timeline = result.current.getManageTimelineById(testId);
- expect(timeline.isLoading).toBeTruthy();
- });
- });
-
- it('getTimelineFilterManager undefined on uninitialized', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- const data = result.current.getTimelineFilterManager(testId);
- expect(data).toEqual(undefined);
- });
- });
-
- it('getTimelineFilterManager defined at initialize', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- result.current.initializeTimeline({
- id: testId,
- filterManager: mockFilterManager,
- });
- const data = result.current.getTimelineFilterManager(testId);
- expect(data).toEqual(mockFilterManager);
- });
- });
-
- it('isManagedTimeline returns false when unset and then true when set', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- let data = result.current.isManagedTimeline(testId);
- expect(data).toBeFalsy();
- result.current.initializeTimeline({
- id: testId,
- filterManager: mockFilterManager,
- });
- data = result.current.isManagedTimeline(testId);
- expect(data).toBeTruthy();
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
deleted file mode 100644
index 1f215ee8f2141..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { createContext, useCallback, useContext, useReducer } from 'react';
-import { noop } from 'lodash/fp';
-
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager';
-import { SubsetTimelineModel } from '../../store/timeline/model';
-import * as i18n from '../../../common/components/events_viewer/translations';
-import * as i18nF from '../timeline/footer/translations';
-import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults';
-
-interface ManageTimelineInit {
- documentType?: string;
- defaultModel?: SubsetTimelineModel;
- filterManager?: FilterManager;
- footerText?: string;
- id: string;
- loadingText?: string;
- selectAll?: boolean;
- queryFields?: string[];
- title?: string;
- unit?: (totalCount: number) => string;
-}
-
-interface ManageTimeline {
- documentType: string;
- defaultModel: SubsetTimelineModel;
- filterManager?: FilterManager;
- footerText: string;
- id: string;
- isLoading: boolean;
- loadingText: string;
- queryFields: string[];
- selectAll: boolean;
- title: string;
- unit: (totalCount: number) => string;
-}
-
-interface ManageTimelineById {
- [id: string]: ManageTimeline;
-}
-const initManageTimeline: ManageTimelineById = {};
-type ActionManageTimeline =
- | {
- type: 'INITIALIZE_TIMELINE';
- id: string;
- payload: ManageTimelineInit;
- }
- | {
- type: 'SET_IS_LOADING';
- id: string;
- payload: boolean;
- }
- | {
- type: 'SET_SELECT_ALL';
- id: string;
- payload: boolean;
- };
-
-export const getTimelineDefaults = (id: string) => ({
- defaultModel: timelineDefaultModel,
- loadingText: i18n.LOADING_EVENTS,
- footerText: i18nF.TOTAL_COUNT_OF_EVENTS,
- documentType: i18nF.TOTAL_COUNT_OF_EVENTS,
- selectAll: false,
- id,
- isLoading: false,
- queryFields: [],
- title: i18n.EVENTS,
- unit: (n: number) => i18n.UNIT(n),
-});
-const reducerManageTimeline = (
- state: ManageTimelineById,
- action: ActionManageTimeline
-): ManageTimelineById => {
- switch (action.type) {
- case 'INITIALIZE_TIMELINE':
- return {
- ...state,
- [action.id]: {
- ...getTimelineDefaults(action.id),
- ...state[action.id],
- ...action.payload,
- },
- } as ManageTimelineById;
- case 'SET_SELECT_ALL':
- return {
- ...state,
- [action.id]: {
- ...state[action.id],
- selectAll: action.payload,
- },
- } as ManageTimelineById;
-
- case 'SET_IS_LOADING':
- return {
- ...state,
- [action.id]: {
- ...state[action.id],
- isLoading: action.payload,
- },
- } as ManageTimelineById;
- default:
- return state;
- }
-};
-
-export interface UseTimelineManager {
- getManageTimelineById: (id: string) => ManageTimeline;
- getTimelineFilterManager: (id: string) => FilterManager | undefined;
- initializeTimeline: (newTimeline: ManageTimelineInit) => void;
- isManagedTimeline: (id: string) => boolean;
- setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void;
- setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void;
-}
-
-export const useTimelineManager = (
- manageTimelineForTesting?: ManageTimelineById
-): UseTimelineManager => {
- const [state, dispatch] = useReducer<
- (state: ManageTimelineById, action: ActionManageTimeline) => ManageTimelineById
- >(reducerManageTimeline, manageTimelineForTesting ?? initManageTimeline);
-
- const initializeTimeline = useCallback((newTimeline: ManageTimelineInit) => {
- dispatch({
- type: 'INITIALIZE_TIMELINE',
- id: newTimeline.id,
- payload: newTimeline,
- });
- }, []);
-
- const setIsTimelineLoading = useCallback(
- ({ id, isLoading }: { id: string; isLoading: boolean }) => {
- dispatch({
- type: 'SET_IS_LOADING',
- id,
- payload: isLoading,
- });
- },
- []
- );
-
- const setSelectAll = useCallback(({ id, selectAll }: { id: string; selectAll: boolean }) => {
- dispatch({
- type: 'SET_SELECT_ALL',
- id,
- payload: selectAll,
- });
- }, []);
-
- const getTimelineFilterManager = useCallback(
- (id: string): FilterManager | undefined => state[id]?.filterManager,
- [state]
- );
- const getManageTimelineById = useCallback(
- (id: string): ManageTimeline => {
- if (state[id] != null) {
- return state[id];
- }
- initializeTimeline({ id });
- return getTimelineDefaults(id);
- },
- [initializeTimeline, state]
- );
- const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]);
-
- return {
- getManageTimelineById,
- getTimelineFilterManager,
- initializeTimeline,
- isManagedTimeline,
- setIsTimelineLoading,
- setSelectAll,
- };
-};
-
-const init = {
- getManageTimelineById: (id: string) => getTimelineDefaults(id),
- getTimelineFilterManager: () => undefined,
- initializeTimeline: () => noop,
- isManagedTimeline: () => false,
- setIsTimelineLoading: () => noop,
- setSelectAll: () => noop,
-};
-
-const ManageTimelineContext = createContext(init);
-
-export const useManageTimeline = () => useContext(ManageTimelineContext);
-
-interface ManageGlobalTimelineProps {
- children: React.ReactNode;
- manageTimelineForTesting?: ManageTimelineById;
-}
-
-export const ManageGlobalTimeline = ({
- children,
- manageTimelineForTesting,
-}: ManageGlobalTimelineProps) => {
- const timelineManager = useTimelineManager(manageTimelineForTesting);
-
- return (
-
- {children}
-
- );
-};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx
index e2c8b8854504a..c73e372b4a71c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx
@@ -62,6 +62,8 @@ import {
} from '../../../network/components/source_destination/field_names';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
index 0544b00a79227..00d2a7b35483e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
@@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiPanel, EuiScreenReaderOnly } from '@elastic/eui';
import React, { useState, useCallback } from 'react';
import styled from 'styled-components';
-import { getNotesContainerClassName } from '../../../../common/components/accessibility/helpers';
+import { getNotesContainerClassName } from '../../../../../../timelines/public';
import { AddNote } from '../add_note';
import { AssociateNote } from '../helpers';
import { NotePreviews, NotePreviewsContainer } from '../../open_timeline/note_previews';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
index c06c3f076e097..c0fea1f210a8a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
@@ -36,7 +36,6 @@ import {
formatTimelineResultToModel,
} from './helpers';
import { OpenTimelineResult, DispatchUpdateTimeline } from './types';
-import { KueryFilterQueryKind } from '../../../common/store';
import { Note } from '../../../common/lib/note';
import moment from 'moment';
import sinon from 'sinon';
@@ -45,6 +44,7 @@ import {
TimelineType,
TimelineStatus,
TimelineTabs,
+ KueryFilterQueryKind,
} from '../../../../common/types/timeline';
import {
mockTimeline as mockSelectedTimeline,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
index e45a1a117769b..03ac0b3d14342 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
@@ -13,6 +13,7 @@ import { Dispatch } from 'redux';
import deepMerge from 'deepmerge';
import {
+ ColumnHeaderOptions,
DataProviderType,
TimelineId,
TimelineStatus,
@@ -37,7 +38,7 @@ import {
addTimeline as dispatchAddTimeline,
addNote as dispatchAddGlobalTimelineNote,
} from '../../../timelines/store/timeline/actions';
-import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
+import { TimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
index 922e40d6d860e..1eafa51058bdd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
@@ -194,7 +194,7 @@ export const OpenTimeline = React.memo(
title={i18n.IMPORT_TIMELINE}
/>
-
+
{!!timelineFilter && timelineFilter}
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ className="euiLoadingContent__singleLineBackground"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
,
.c1 {
@@ -554,249 +509,204 @@ Array [
onClose={[Function]}
size="m"
>
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ className="euiLoadingContent__singleLineBackground"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
,
- .c1 {
+ .c0 {
-webkit-flex: 0 1 auto;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
margin-top: 8px;
}
-.c2 .euiFlyoutBody__overflow {
+.c1 .euiFlyoutBody__overflow {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@@ -807,7 +717,7 @@ Array [
overflow: hidden;
}
-.c2 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent {
+.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent {
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
@@ -815,54 +725,16 @@ Array [
padding: 4px 16px 50px;
}
-.c0 {
- z-index: 7000;
-}
-
-
-
-
-
-
-
-
-
-
+
= styled(
+ EuiFlyout
+)`
z-index: ${({ theme }) => theme.eui.euiZLevel7};
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx
index 9887563c0fef6..2daebdf37e77f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx
@@ -16,41 +16,28 @@ import {
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
+import {
+ HeaderActionProps,
+ SortDirection,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations';
import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants';
import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../../../common/containers/use_full_screen';
-import { BrowserFields } from '../../../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
-import { OnSelectAll } from '../../events';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { StatefulFieldsBrowser } from '../../../fields_browser';
import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers';
import { EventsTh, EventsThContent } from '../../styles';
-import { Sort, SortDirection } from '../sort';
import { EventsSelect } from '../column_headers/events_select';
import * as i18n from '../column_headers/translations';
import { timelineActions } from '../../../../store/timeline';
import { isFullScreen } from '../column_headers';
-export interface HeaderActionProps {
- width: number;
- browserFields: BrowserFields;
- columnHeaders: ColumnHeaderOptions[];
- isEventViewer?: boolean;
- isSelectAllChecked: boolean;
- onSelectAll: OnSelectAll;
- showEventsSelect: boolean;
- showSelectAllCheckbox: boolean;
- sort: Sort[];
- tabType: TimelineTabs;
- timelineId: string;
-}
-
const SortingColumnsContainer = styled.div`
button {
color: ${({ theme }) => theme.eui.euiColorPrimary};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
index a186b324cc03a..82d593e80bc44 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
@@ -41,8 +41,6 @@ describe('Actions', () => {
eventId="abc"
loadingEventIds={[]}
onEventDetailsPanelOpened={jest.fn()}
- onPinEvent={jest.fn()}
- onUnPinEvent={jest.fn()}
onRowSelected={jest.fn()}
showNotes={false}
isEventPinned={false}
@@ -74,8 +72,6 @@ describe('Actions', () => {
toggleShowNotes={jest.fn()}
timelineId={'test'}
refetch={jest.fn()}
- onPinEvent={jest.fn()}
- onUnPinEvent={jest.fn()}
columnId={''}
index={2}
eventId="abc"
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
index 2053b9a0da942..0a3a1cd88accc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
@@ -6,7 +6,9 @@
*/
import React, { useCallback, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
+import { noop } from 'lodash/fp';
import {
eventHasNotes,
getEventType,
@@ -22,45 +24,9 @@ import * as i18n from '../translations';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action';
-import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
-import { timelineSelectors } from '../../../../store/timeline';
+import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/types/timeline';
+import { timelineActions, timelineSelectors } from '../../../../store/timeline';
import { timelineDefaults } from '../../../../store/timeline/defaults';
-import { Ecs } from '../../../../../../common/ecs';
-import { inputsModel } from '../../../../../common/store';
-import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
-import { RowCellRender } from '../control_columns';
-
-interface Props {
- ariaRowindex: number;
- action?: RowCellRender;
- width?: number;
- columnId: string;
- columnValues: string;
- checked: boolean;
- onRowSelected: OnRowSelected;
- eventId: string;
- loadingEventIds: Readonly
;
- onEventDetailsPanelOpened: () => void;
- showCheckboxes: boolean;
- data: TimelineNonEcsData[];
- ecsData: Ecs;
- index: number;
- eventIdToNoteIds: Readonly>;
- isEventPinned: boolean;
- isEventViewer?: boolean;
- onPinEvent: OnPinEvent;
- onUnPinEvent: OnUnPinEvent;
- refetch: inputsModel.Refetch;
- rowIndex: number;
- onRuleChange?: () => void;
- showNotes: boolean;
- tabType?: TimelineTabs;
- timelineId: string;
- toggleShowNotes: () => void;
-}
-
-export type ActionProps = Props;
const ActionsComponent: React.FC = ({
ariaRowindex,
@@ -75,9 +41,7 @@ const ActionsComponent: React.FC = ({
isEventViewer = false,
loadingEventIds,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
onRuleChange,
showCheckboxes,
@@ -85,9 +49,20 @@ const ActionsComponent: React.FC = ({
timelineId,
toggleShowNotes,
}) => {
+ const dispatch = useDispatch();
const emptyNotes: string[] = [];
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+ const onPinEvent: OnPinEvent = useCallback(
+ (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })),
+ [dispatch, timelineId]
+ );
+
+ const onUnPinEvent: OnPinEvent = useCallback(
+ (evtId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId: evtId })),
+ [dispatch, timelineId]
+ );
+
const handleSelectEvent = useCallback(
(event: React.ChangeEvent) =>
onRowSelected({
@@ -99,7 +74,7 @@ const ActionsComponent: React.FC = ({
const handlePinClicked = useCallback(
() =>
getPinOnClick({
- allowUnpinning: !eventHasNotes(eventIdToNoteIds[eventId]),
+ allowUnpinning: eventIdToNoteIds ? !eventHasNotes(eventIdToNoteIds[eventId]) : true,
eventId,
onPinEvent,
onUnPinEvent,
@@ -164,12 +139,12 @@ const ActionsComponent: React.FC = ({
/>
)}
- {!isEventViewer && (
+ {!isEventViewer && toggleShowNotes && (
<>
@@ -177,7 +152,7 @@ const ActionsComponent: React.FC = ({
ariaLabel={i18n.PIN_EVENT_FOR_ROW({ ariaRowindex, columnValues, isEventPinned })}
key="pin-event"
onPinClicked={handlePinClicked}
- noteIds={eventIdToNoteIds[eventId] || emptyNotes}
+ noteIds={eventIdToNoteIds ? eventIdToNoteIds[eventId] || emptyNotes : emptyNotes}
eventIsPinned={isEventPinned}
timelineType={timelineType}
/>
@@ -200,7 +175,7 @@ const ActionsComponent: React.FC = ({
ecsRowData={ecsData}
timelineId={timelineId}
disabled={eventType !== 'signal' && !isEventContextMenuEnabled}
- refetch={refetch}
+ refetch={refetch ?? noop}
onRuleChange={onRuleChange}
/>
>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx
index f9eda55c237ae..8795255dfcfd4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx
@@ -8,7 +8,7 @@
import { EuiButtonIcon } from '@elastic/eui';
import React, { useCallback } from 'react';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { OnColumnRemoved } from '../../../events';
import { EventsHeadingExtra, EventsLoading } from '../../../styles';
import { Sort } from '../../sort';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx
index 3ab4d564391f3..74593e40ddf4c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx
@@ -12,16 +12,12 @@ import { Resizable, ResizeCallback } from 're-resizable';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
-import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants';
-import {
- DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
- getDraggableFieldId,
-} from '../../../../../common/components/drag_and_drop/helpers';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
+import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers';
+import { ColumnHeaderOptions, TimelineTabs } from '../../../../../../common/types/timeline';
import { Direction } from '../../../../../../common/search_strategy';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import { OnFilterChange } from '../../events';
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
@@ -31,6 +27,7 @@ import { Header } from './header';
import { timelineActions } from '../../../../store/timeline';
import * as i18n from './translations';
+import { useKibana } from '../../../../../common/lib/kibana';
const ContextMenu = styled(EuiContextMenu)`
width: 115px;
@@ -75,6 +72,7 @@ const ColumnHeaderComponent: React.FC = ({
const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []);
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const resizableSize = useMemo(
() => ({
width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH,
@@ -247,7 +245,7 @@ const ColumnHeaderComponent: React.FC = ({
setHoverActionsOwnFocus(true);
}, []);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId,
fieldName: header.id,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts
index fea65d0499a13..7eb98b7475952 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { ColumnHeaderOptions, ColumnHeaderType } from '../../../../store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../common';
+import { ColumnHeaderType } from '../../../../store/timeline/model';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx
index bdf4cc42fa794..828b8d8701188 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx
@@ -8,9 +8,9 @@
import { noop } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../constants';
import { OnFilterChange } from '../../../events';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
import { TextFilter } from '../text_filter';
interface Props {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx
index 484cb78417c2f..ffab38b64bef8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx
@@ -8,8 +8,8 @@
import { EuiToolTip } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
import { TruncatableText } from '../../../../../../common/components/truncatable_text';
import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles';
import { Sort } from '../../sort';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts
index b52fa292413df..257b88944c14e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts
@@ -6,9 +6,8 @@
*/
import { Direction } from '../../../../../../../common/search_strategy';
-import { assertUnreachable } from '../../../../../../../common/utility_types';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
-import { Sort, SortDirection } from '../../sort';
+import { ColumnHeaderOptions, SortDirection } from '../../../../../../../common/types/timeline';
+import { Sort } from '../../sort';
interface GetNewSortDirectionOnClickParams {
clickedHeader: ColumnHeaderOptions;
@@ -35,7 +34,7 @@ export const getNextSortDirection = (currentSort: Sort): Direction => {
case 'none':
return Direction.desc;
default:
- return assertUnreachable(currentSort.sortDirection, 'Unhandled sort direction');
+ return Direction.desc;
}
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx
index f2496484c25ea..4fa72fa5da424 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx
@@ -18,6 +18,7 @@ import { defaultHeaders } from '../default_headers';
import { HeaderComponent } from '.';
import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers';
import { Direction } from '../../../../../../../common/search_strategy';
+import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
@@ -30,6 +31,11 @@ jest.mock('react-redux', () => {
};
});
+jest.mock('../../../../../../common/hooks/use_selector', () => ({
+ useShallowEqualSelector: jest.fn(),
+ useDeepEqualSelector: jest.fn(),
+}));
+
const filteredColumnHeader: ColumnHeaderType = 'text-filter';
describe('Header', () => {
@@ -41,7 +47,11 @@ describe('Header', () => {
sortDirection: Direction.desc,
},
];
- const timelineId = 'fakeId';
+ const timelineId = 'test';
+
+ beforeEach(() => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false });
+ });
test('renders correctly against snapshot', () => {
const wrapper = shallow(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
index ece28faedb951..60a241a340d99 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
@@ -9,16 +9,18 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
-import { useShallowEqualSelector } from '../../../../../../common/hooks/use_selector';
-import { timelineActions } from '../../../../../store/timeline';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
+import {
+ useDeepEqualSelector,
+ useShallowEqualSelector,
+} from '../../../../../../common/hooks/use_selector';
+import { timelineActions, timelineSelectors } from '../../../../../store/timeline';
import { OnFilterChange } from '../../../events';
import { Sort } from '../../sort';
import { Actions } from '../actions';
import { Filter } from '../filter';
import { getNewSortDirectionOnClick } from './helpers';
import { HeaderContent } from './header_content';
-import { useManageTimeline } from '../../../../manage_timeline';
import { isEqlOnSelector } from './selectors';
interface Props {
@@ -80,12 +82,10 @@ export const HeaderComponent: React.FC = ({
[dispatch, timelineId]
);
- const { getManageTimelineById } = useManageTimeline();
-
- const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector(
+ (state) => getManageTimeline(state, timelineId) || { isLoading: false }
+ );
const showSortingCapability = !isEqlOn && !(header.subType && header.subType.nested);
return (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx
index 5b5a8b10591d4..b33e47dd27b96 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx
@@ -9,9 +9,8 @@ import { mount, shallow } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { defaultHeaders } from '../../../../../../common/mock';
-
import { HeaderToolTipContent } from '.';
describe('HeaderToolTipContent', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx
index f4e7b6459bd14..0ae8dbb537fb8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx
@@ -10,7 +10,7 @@ import { isEmpty } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { getIconFromType } from '../../../../../../common/components/event_details/helpers';
import * as i18n from '../translations';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
index d19c5689ab049..c49d088d6241d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
@@ -6,9 +6,9 @@
*/
import { get } from 'lodash/fp';
+import { ColumnHeaderOptions } from '../../../../../../common';
import { BrowserFields } from '../../../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx
index 41f9db3f1c25b..378f7fce250fe 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx
@@ -24,6 +24,8 @@ import { Direction } from '../../../../../../common/search_strategy';
import { defaultControlColumn } from '../control_columns';
import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns';
+jest.mock('../../../../../common/lib/kibana');
+
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
index 3b0b935bfcff4..25aefd513f806 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
@@ -11,12 +11,17 @@ import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd';
import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper';
import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge';
import { BrowserFields } from '../../../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import {
DRAG_TYPE_FIELD,
droppableTimelineColumnsPrefix,
} from '../../../../../common/components/drag_and_drop/helpers';
-import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
+import {
+ ColumnHeaderOptions,
+ ControlColumnProps,
+ HeaderActionProps,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { OnSelectAll } from '../../events';
import {
EventsTh,
@@ -27,8 +32,6 @@ import {
} from '../../styles';
import { Sort } from '../sort';
import { ColumnHeader } from './column_header';
-import { ControlColumnProps } from '../control_columns';
-import { HeaderActionProps } from '../actions/header_actions';
interface Props {
actionsColumnWidth: number;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx
index 8ef69697af1d0..e4f4c26417351 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx
@@ -5,48 +5,9 @@
* 2.0.
*/
-import { ComponentType, JSXElementConstructor } from 'react';
-import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui';
-import { OnRowSelected } from '../../events';
-import { ActionProps, Actions } from '../actions';
-import { HeaderActions, HeaderActionProps } from '../actions/header_actions';
-
-export type GenericActionRowCellRenderProps = Pick<
- EuiDataGridCellValueElementProps,
- 'rowIndex' | 'columnId'
->;
-
-export type HeaderCellRender = ComponentType | ComponentType;
-export type RowCellRender =
- | JSXElementConstructor
- | ((props: GenericActionRowCellRenderProps) => JSX.Element)
- | JSXElementConstructor
- | ((props: ActionProps) => JSX.Element);
-
-interface AdditionalControlColumnProps {
- ariaRowindex: number;
- actionsColumnWidth: number;
- columnValues: string;
- checked: boolean;
- onRowSelected: OnRowSelected;
- eventId: string;
- id: string;
- columnId: string;
- loadingEventIds: Readonly;
- onEventDetailsPanelOpened: () => void;
- showCheckboxes: boolean;
- // Override these type definitions to support either a generic custom component or the one used in security_solution today.
- headerCellRender: HeaderCellRender;
- rowCellRender: RowCellRender;
- // If not provided, calculated dynamically
- width?: number;
-}
-
-export type ControlColumnProps = Omit<
- EuiDataGridControlColumn,
- keyof AdditionalControlColumnProps
-> &
- Partial;
+import { ControlColumnProps } from '../../../../../../common/types/timeline';
+import { Actions } from '../actions';
+import { HeaderActions } from '../actions/header_actions';
export const defaultControlColumn: ControlColumnProps = {
id: 'default-timeline-control-column',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx
index ae6307c0a294b..ecacbc51e395a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx
@@ -36,11 +36,9 @@ describe('Columns', () => {
timelineId="test"
columnValues={'abc def'}
showCheckboxes={false}
- onPinEvent={jest.fn()}
selectedEventIds={{}}
loadingEventIds={[]}
onEventDetailsPanelOpened={jest.fn()}
- onUnPinEvent={jest.fn()}
onRowSelected={jest.fn()}
showNotes={false}
isEventPinned={false}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
index ecabc3eae51c4..11bf88977fe61 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
@@ -8,17 +8,20 @@
import { EuiScreenReaderOnly } from '@elastic/eui';
import React, { useMemo } from 'react';
import { getOr } from 'lodash/fp';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps, RowCellRender } from '../control_columns';
-import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers';
import { Ecs } from '../../../../../../common/ecs';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ActionProps,
+ ControlColumnProps,
+ TimelineTabs,
+ RowCellRender,
+} from '../../../../../../common/types/timeline';
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
-import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
-import { ActionProps } from '../actions';
+import { OnRowSelected } from '../../events';
import { inputsModel } from '../../../../../common/store';
import {
EventsTd,
@@ -60,9 +63,7 @@ interface DataDrivenColumnProps {
loadingEventIds: Readonly;
notesCount: number;
onEventDetailsPanelOpened: () => void;
- onPinEvent: OnPinEvent;
onRowSelected: OnRowSelected;
- onUnPinEvent: OnUnPinEvent;
refetch: inputsModel.Refetch;
onRuleChange?: () => void;
hasRowRenderers: boolean;
@@ -137,9 +138,7 @@ const TgridActionTdCell = ({
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
rowIndex,
hasRowRenderers,
@@ -193,9 +192,7 @@ const TgridActionTdCell = ({
isEventViewer={isEventViewer}
loadingEventIds={loadingEventIds}
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
- onPinEvent={onPinEvent}
onRowSelected={onRowSelected}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
rowIndex={rowIndex}
onRuleChange={onRuleChange}
@@ -292,9 +289,7 @@ export const DataDrivenColumns = React.memo(
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
hasRowRenderers,
onRuleChange,
@@ -345,8 +340,6 @@ export const DataDrivenColumns = React.memo(
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
notesCount={notesCount}
- onPinEvent={onPinEvent}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
hasRowRenderers={hasRowRenderers}
onRuleChange={onRuleChange}
@@ -365,7 +358,6 @@ export const DataDrivenColumns = React.memo(
data,
ecsData,
onRowSelected,
- onPinEvent,
isEventPinned,
isEventViewer,
actionsColumnWidth,
@@ -378,7 +370,6 @@ export const DataDrivenColumns = React.memo(
notesCount,
onEventDetailsPanelOpened,
onRuleChange,
- onUnPinEvent,
refetch,
selectedEventIds,
showCheckboxes,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
index 3c75bc7fb2649..3e22cba208ca2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
@@ -9,11 +9,13 @@ import { mount } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React, { useEffect } from 'react';
-import { CellValueElementProps } from '../../cell_rendering';
import { defaultHeaders, mockTimelineData } from '../../../../../common/mock';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { StatefulCell } from './stateful_cell';
import { getMappedNonEcsValue } from '.';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx
index a5f8336cc7997..7931e0739aa68 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx
@@ -7,10 +7,12 @@
import React, { HTMLAttributes, useState } from 'react';
-import { CellValueElementProps } from '../../cell_rendering';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
export interface CommonProps {
className?: string;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
index e56171aae003c..17f231c0fdad9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
@@ -60,9 +60,7 @@ describe('EventColumnView', () => {
loadingEventIds: [],
notesCount: 0,
onEventDetailsPanelOpened: jest.fn(),
- onPinEvent: jest.fn(),
onRowSelected: jest.fn(),
- onUnPinEvent: jest.fn(),
refetch: jest.fn(),
renderCellValue: DefaultCellRenderer,
selectedEventIds: {},
@@ -120,16 +118,6 @@ describe('EventColumnView', () => {
expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false);
});
- test('it invokes onPinClicked when the button for pinning events is clicked', () => {
- const wrapper = mount( , { wrappingComponent: TestProviders });
-
- expect(props.onPinEvent).not.toHaveBeenCalled();
-
- wrapper.find('[data-test-subj="pin"]').first().simulate('click');
-
- expect(props.onPinEvent).toHaveBeenCalled();
- });
-
test('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => {
const wrapper = mount( , {
wrappingComponent: TestProviders,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
index 5dc718f90a91a..298ce252ba925 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
@@ -7,16 +7,19 @@
import React, { useMemo } from 'react';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps, RowCellRender } from '../control_columns';
import { Ecs } from '../../../../../../common/ecs';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
-import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
+import { OnRowSelected } from '../../events';
import { EventsTrData, EventsTdGroupActions } from '../../styles';
import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns';
import { inputsModel } from '../../../../../common/store';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ControlColumnProps,
+ RowCellRender,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
interface Props {
id: string;
@@ -31,9 +34,7 @@ interface Props {
loadingEventIds: Readonly;
notesCount: number;
onEventDetailsPanelOpened: () => void;
- onPinEvent: OnPinEvent;
onRowSelected: OnRowSelected;
- onUnPinEvent: OnUnPinEvent;
refetch: inputsModel.Refetch;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
onRuleChange?: () => void;
@@ -62,9 +63,7 @@ export const EventColumnView = React.memo(
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
hasRowRenderers,
onRuleChange,
@@ -134,10 +133,8 @@ export const EventColumnView = React.memo(
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
- onPinEvent={onPinEvent}
- onUnPinEvent={onUnPinEvent}
- refetch={refetch}
onRuleChange={onRuleChange}
+ refetch={refetch}
showNotes={showNotes}
tabType={tabType}
timelineId={timelineId}
@@ -161,10 +158,8 @@ export const EventColumnView = React.memo(
leadingControlColumns,
loadingEventIds,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
onRuleChange,
- onUnPinEvent,
refetch,
selectedEventIds,
showCheckboxes,
@@ -201,8 +196,6 @@ export const EventColumnView = React.memo(
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
- onPinEvent={onPinEvent}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
index c3097ad68aba1..c09de87c87f32 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
@@ -8,19 +8,21 @@
import React from 'react';
import { isEmpty } from 'lodash';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps } from '../control_columns';
import { inputsModel } from '../../../../../common/store';
import { BrowserFields } from '../../../../../common/containers/source';
import {
TimelineItem,
TimelineNonEcsData,
} from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ControlColumnProps,
+ RowRenderer,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { OnRowSelected } from '../../events';
import { EventsTbody } from '../../styles';
-import { RowRenderer } from '../renderers/row_renderer';
import { StatefulEvent } from './stateful_event';
import { eventIsPinned } from '../helpers';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
index 701dc549467e9..b8840a75cc9b4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
@@ -8,10 +8,12 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps } from '../control_columns';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ControlColumnProps,
+ RowRenderer,
TimelineExpandedDetailType,
TimelineId,
TimelineTabs,
@@ -21,11 +23,9 @@ import {
TimelineItem,
TimelineNonEcsData,
} from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
-import { OnPinEvent, OnRowSelected } from '../../events';
+import { OnRowSelected } from '../../events';
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers';
import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles';
-import { RowRenderer } from '../renderers/row_renderer';
import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers';
import { NoteCards } from '../../../notes/note_cards';
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
@@ -176,16 +176,6 @@ const StatefulEventComponent: React.FC = ({
});
}, [event]);
- const onPinEvent: OnPinEvent = useCallback(
- (eventId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId })),
- [dispatch, timelineId]
- );
-
- const onUnPinEvent: OnPinEvent = useCallback(
- (eventId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId })),
- [dispatch, timelineId]
- );
-
const handleOnEventDetailPanelOpened = useCallback(() => {
const eventId = event._id;
const indexName = event._index!;
@@ -215,10 +205,10 @@ const StatefulEventComponent: React.FC = ({
(noteId: string) => {
dispatch(timelineActions.addNoteToEvent({ eventId: event._id, id: timelineId, noteId }));
if (!isEventPinned) {
- onPinEvent(event._id); // pin the event, because it has notes
+ dispatch(timelineActions.pinEvent({ id: timelineId, eventId: event._id }));
}
},
- [dispatch, event, isEventPinned, onPinEvent, timelineId]
+ [dispatch, event, isEventPinned, timelineId]
);
const RowRendererContent = useMemo(
@@ -273,9 +263,7 @@ const StatefulEventComponent: React.FC = ({
loadingEventIds={loadingEventIds}
notesCount={notes.length}
onEventDetailsPanelOpened={handleOnEventDetailPanelOpened}
- onPinEvent={onPinEvent}
onRowSelected={onRowSelected}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
renderCellValue={renderCellValue}
onRuleChange={onRuleChange}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx
index 10a25538c1ba3..19abd6841e7e8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx
@@ -9,15 +9,15 @@ import { noop } from 'lodash/fp';
import { EuiFocusTrap, EuiOutsideClickDetector, EuiScreenReaderOnly } from '@elastic/eui';
import React, { useMemo } from 'react';
-import { BrowserFields } from '../../../../../../common/containers/source';
import {
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
getRowRendererClassName,
-} from '../../../../../../common/components/accessibility/helpers';
+} from '../../../../../../../../timelines/public';
+import { RowRenderer } from '../../../../../../../common';
+import { BrowserFields } from '../../../../../../common/containers/source';
import { TimelineItem } from '../../../../../../../common/search_strategy/timeline';
import { getRowRenderer } from '../../renderers/get_row_renderer';
-import { RowRenderer } from '../../renderers/row_renderer';
import { useStatefulEventFocus } from '../use_stateful_event_focus';
import * as i18n from '../translations';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx
index 5f3c4dac8b73d..4e8fd7dc48968 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx
@@ -13,7 +13,7 @@ import {
isEscape,
focusColumn,
OnColumnFocused,
-} from '../../../../../../common/components/accessibility/helpers';
+} from '../../../../../../../../timelines/public';
type FocusOwnership = 'not-owned' | 'owned';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
index 61601c3921445..19059b5fb4599 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
@@ -23,6 +23,8 @@ import { timelineActions } from '../../../store/timeline';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { defaultRowRenderers } from './renderers';
+jest.mock('../../../../common/lib/kibana');
+
const mockSort: Sort[] = [
{
columnId: '@timestamp',
@@ -255,7 +257,7 @@ describe('Body', () => {
tabType: 'query',
timelineId: 'timeline-test',
},
- type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
+ type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL',
});
});
@@ -279,7 +281,7 @@ describe('Body', () => {
tabType: 'pinned',
timelineId: 'timeline-test',
},
- type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
+ type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL',
});
});
@@ -303,7 +305,7 @@ describe('Body', () => {
tabType: 'notes',
timelineId: 'timeline-test',
},
- type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
+ type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL',
});
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
index 64f61232377e8..fc8bf2086471c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
@@ -11,21 +11,26 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { connect, ConnectedProps } from 'react-redux';
import deepEqual from 'fast-deep-equal';
-import { CellValueElementProps } from '../cell_rendering';
-import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
-import { ControlColumnProps } from './control_columns';
-import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
import {
FIRST_ARIA_INDEX,
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
-} from '../../../../common/components/accessibility/helpers';
+} from '../../../../../../timelines/public';
+import { CellValueElementProps } from '../cell_rendering';
+import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
+import {
+ ColumnHeaderOptions,
+ ControlColumnProps,
+ RowRendererId,
+ RowRenderer,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
import { TimelineItem } from '../../../../../common/search_strategy/timeline';
import { inputsModel, State } from '../../../../common/store';
-import { useManageTimeline } from '../../manage_timeline';
-import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model';
+import { TimelineModel } from '../../../store/timeline/model';
import { timelineDefaults } from '../../../store/timeline/defaults';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { OnRowSelected, OnSelectAll } from '../events';
@@ -33,11 +38,11 @@ import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helper
import { getEventIdToDataMapping } from './helpers';
import { Sort } from './sort';
import { plainRowRenderer } from './renderers/plain_row_renderer';
-import { RowRenderer } from './renderers/row_renderer';
import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles';
import { ColumnHeaders } from './column_headers';
import { Events } from './events';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
interface OwnProps {
activePage: number;
@@ -99,11 +104,10 @@ export const BodyComponent = React.memo(
trailingControlColumns = [],
}) => {
const containerRef = useRef(null);
- const { getManageTimelineById } = useManageTimeline();
- const { queryFields, selectAll } = useMemo(() => getManageTimelineById(id), [
- getManageTimelineById,
- id,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { queryFields, selectAll } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, id)
+ );
const onRowSelected: OnRowSelected = useCallback(
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx
index 21c44cb26e2e5..d5ec8b6f94862 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { TestProviders } from '../../../../../common/mock';
import { ArgsComponent } from './args';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx
index f45c049ca137a..2a5764e53756a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx
@@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { AuditdGenericDetails, AuditdGenericLine } from './generic_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx
index 51676c067cd79..009ffecf28f74 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx
@@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { AuditdGenericFileDetails, AuditdGenericFileLine } from './generic_file_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
index 31fea6fa25e65..74a5ff472b581 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
@@ -9,17 +9,19 @@ import { shallow } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
+import { RowRenderer } from '../../../../../../../common';
import { BrowserFields } from '../../../../../../common/containers/source';
import { mockBrowserFields } from '../../../../../../common/containers/source/mock';
import { Ecs } from '../../../../../../../common/ecs';
import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
-import { RowRenderer } from '../row_renderer';
import {
createGenericAuditRowRenderer,
createGenericFileRowRenderer,
} from './generic_row_renderer';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
index 9133e500162bc..765bfd3d21351 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
@@ -11,9 +11,9 @@ import { IconType } from '@elastic/eui';
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { AuditdGenericDetails } from './generic_details';
import { AuditdGenericFileDetails } from './generic_file_details';
import * as i18n from './translations';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx
index 24b9f8d40eb17..d6037a310dc7e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx
index 22cd8446a51c0..fa6eda6bce37d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx
@@ -14,6 +14,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { SessionUserHostWorkingDir } from './session_user_host_working_dir';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx
index 8b4a9f72b1a45..c7da6f758766e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { Bytes } from '.';
+jest.mock('../../../../../../common/lib/kibana');
+
describe('Bytes', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts
index cb670b53a9679..65bb67458ab2a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts
@@ -6,8 +6,8 @@
*/
import type React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../common';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
export interface ColumnRenderer {
isInstance: (columnName: string, data: TimelineNonEcsData[]) => boolean;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx
index 7f580642130fe..872ca017d7f7d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx
@@ -12,6 +12,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { ThreatMatchRowProps, ThreatMatchRowView } from './threat_match_row';
+jest.mock('../../../../../../common/lib/kibana');
+
describe('ThreatMatchRowView', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx
index 2a7e8ce02d79f..16426bf74aba7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx
@@ -5,8 +5,7 @@
* 2.0.
*/
-import { RowRendererId } from '../../../../../../../common/types/timeline';
-import { RowRenderer } from '../row_renderer';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
import { hasThreatMatchValue } from './helpers';
import { ThreatMatchRows } from './threat_match_rows';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx
index cc34f9e63b5e2..f6feb6dd1b126 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx
@@ -10,9 +10,10 @@ import { get } from 'lodash';
import React, { Fragment } from 'react';
import styled from 'styled-components';
+import { RowRenderer } from '../../../../../../../common';
import { Fields } from '../../../../../../../common/search_strategy';
import { ID_FIELD_NAME } from '../../../../../../common/components/event_details/event_id';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { ThreatMatchRow } from './threat_match_row';
const SpacedContainer = styled.div`
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx
index d3e870aa92ef0..9e6c5b819a20b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx
@@ -15,6 +15,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { DnsRequestEventDetails } from './dns_request_event_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx
index 2809b06c77469..5c0aecf5fbbc7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx
@@ -12,6 +12,8 @@ import '../../../../../../common/mock/match_media';
import { DnsRequestEventDetailsLine } from './dns_request_event_details_line';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx
index 034ade75ef2c0..5144705f26174 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx
@@ -18,6 +18,8 @@ import { getEmptyValue } from '../../../../../common/components/empty_value';
import { deleteItemIdx, findItem } from './helpers';
import { emptyColumnRenderer } from './empty_column_renderer';
+jest.mock('../../../../../common/lib/kibana');
+
describe('empty_column_renderer', () => {
let mockDatum: TimelineNonEcsData[];
const _id = mockTimelineData[0]._id;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx
index 400ccf47201ac..37873df7f4e7b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx
@@ -8,9 +8,8 @@
/* eslint-disable react/display-name */
import React from 'react';
-
+import { ColumnHeaderOptions } from '../../../../../../common';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import {
DraggableWrapper,
DragEffects,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx
index c1df6d6eb48c8..613d66505601a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx
@@ -20,6 +20,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { EndgameSecurityEventDetails } from './endgame_security_event_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx
index 5d08898789821..879862d06b250 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx
@@ -13,6 +13,8 @@ import '../../../../../../common/mock/match_media';
import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx
index a6f15a9f79f4e..1bf8d1a4a4f51 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { ExitCodeDraggable } from './exit_code_draggable';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx
index d7274f0774fc5..cf3fce2c25c0b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../common/mock';
import { FileDraggable } from './file_draggable';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx
index e7e6274942bea..8ebd3ae8a67c2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { FileHash } from './file_hash';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx
index 8e54f13ec9cbf..852331aa021dd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx
@@ -21,6 +21,8 @@ import { getColumnRenderer } from './get_column_renderer';
import { getValues, findItem, deleteItemIdx } from './helpers';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx
index 56dbc99d47c66..104550f138f16 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx
@@ -20,6 +20,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { defaultRowRenderers } from '.';
import { getRowRenderer } from './get_row_renderer';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts
index bfe60a14e042d..2d1be6ee7914a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
+import { RowRenderer } from '../../../../../../common';
import { Ecs } from '../../../../../../common/ecs';
-import { RowRenderer } from './row_renderer';
export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null =>
rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx
index 9412ecfd364ba..d650710b25cad 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx
@@ -13,6 +13,8 @@ import { mockTimelineData, TestProviders } from '../../../../../common/mock';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
import { HostWorkingDir } from './host_working_dir';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts
index 537a24bbfd953..911dcc8cd2e87 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts
@@ -5,12 +5,12 @@
* 2.0.
*/
+import { RowRenderer } from '../../../../../../common';
import { auditdRowRenderers } from './auditd/generic_row_renderer';
import { ColumnRenderer } from './column_renderer';
import { emptyColumnRenderer } from './empty_column_renderer';
import { netflowRowRenderer } from './netflow/netflow_row_renderer';
import { plainColumnRenderer } from './plain_column_renderer';
-import { RowRenderer } from './row_renderer';
import { suricataRowRenderer } from './suricata/suricata_row_renderer';
import { unknownColumnRenderer } from './unknown_column_renderer';
import { zeekRowRenderer } from './zeek/zeek_row_renderer';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx
index 72e3516827c8a..fc97624dbfc96 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx
@@ -26,6 +26,8 @@ export const justIdAndTimestamp: Ecs = {
timestamp: '2018-11-12T19:03:25.936Z',
};
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('../../../../../../common/components/link_to');
describe('netflowRowRenderer', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
index 2605670ee8b38..35406dce6ff72 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
@@ -11,7 +11,7 @@ import { get } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
import { asArrayIfExists } from '../../../../../../common/lib/helpers';
import {
TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME,
@@ -63,7 +63,7 @@ import {
SOURCE_BYTES_FIELD_NAME,
SOURCE_PACKETS_FIELD_NAME,
} from '../../../../../../network/components/source_destination/source_destination_arrows';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
const Details = styled.div`
margin: 5px 0;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx
index 2402be88dea18..7c28747cc84ef 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../common/mock';
import { ParentProcessDraggable } from './parent_process_draggable';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx
index a56acbe48685c..e970aaad026b1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx
@@ -18,6 +18,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { plainColumnRenderer } from './plain_column_renderer';
import { getValues, deleteItemIdx, findItem } from './helpers';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx
index a2b7750d9bb59..77039ddc4a586 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx
@@ -8,8 +8,8 @@
import { head } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../common';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import { ColumnRenderer } from './column_renderer';
import { FormattedFieldValue } from './formatted_field';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
index 0b5afd579d08c..15620a7fc04b4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
@@ -7,9 +7,7 @@
import React from 'react';
-import { RowRendererId } from '../../../../../../common/types/timeline';
-
-import { RowRenderer } from './row_renderer';
+import { RowRendererId, RowRenderer } from '../../../../../../common/types/timeline';
const PlainRowRenderer = () => <>>;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx
index 31a1745fa2a6d..6509808fb0c9f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx
@@ -13,6 +13,8 @@ import '../../../../../common/mock/match_media';
import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx
index 9e90e061e94d5..7135f2a5fed6a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { ProcessHash } from './process_hash';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx
index f37adef7e73cb..e5bb91c532505 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx
@@ -18,6 +18,8 @@ import { MODIFIED_REGISTRY_KEY } from '../system/translations';
import { RegistryEventDetails } from './registry_event_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx
index 6be1529152523..d0287f2b010ae 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { RegistryEventDetailsLine } from './registry_event_details_line';
import { MODIFIED_REGISTRY_KEY } from '../system/translations';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
index 679da28e622bf..9099f76b8305c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
@@ -7,11 +7,7 @@
import React from 'react';
-import { BrowserFields } from '../../../../../common/containers/source';
-import type { RowRendererId } from '../../../../../../common/types/timeline';
-import { Ecs } from '../../../../../../common/ecs';
import { EventsTrSupplement } from '../../styles';
-
interface RowRendererContainerProps {
children: React.ReactNode;
}
@@ -22,17 +18,3 @@ export const RowRendererContainer = React.memo(({ chi
));
RowRendererContainer.displayName = 'RowRendererContainer';
-
-export interface RowRenderer {
- id: RowRendererId;
- isInstance: (data: Ecs) => boolean;
- renderRow: ({
- browserFields,
- data,
- timelineId,
- }: {
- browserFields: BrowserFields;
- data: Ecs;
- timelineId: string;
- }) => React.ReactNode;
-}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx
index 5960f43174b98..355077ee50066 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx
@@ -16,6 +16,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { SuricataDetails } from './suricata_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx
index 098d6775cfaa4..998233b2278c9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx
@@ -18,6 +18,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers';
import { suricataRowRenderer } from './suricata_row_renderer';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
index 5a68bc6fe28c8..aa482926bf007 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
@@ -10,9 +10,9 @@
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { SuricataDetails } from './suricata_details';
export const suricataRowRenderer: RowRenderer = {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx
index 4a727e4e7bc27..b3911f9eded67 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx
@@ -18,6 +18,8 @@ import {
SURICATA_SIGNATURE_ID_FIELD_NAME,
} from './suricata_signature';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx
index 001b7f4b68bab..35872d0093f02 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx
@@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { SystemGenericDetails, SystemGenericLine } from './generic_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx
index b660d823954ee..f5dc4c6fdf599 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx
@@ -16,6 +16,8 @@ import { mockEndgameCreationEvent } from '../../../../../../common/mock/mock_end
import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
index 8e8ce9cb2f988..6f5b225f0690b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
@@ -67,7 +67,6 @@ import {
mockEndpointSecurityLogOffEvent,
} from '../../../../../../common/mock/mock_endgame_ecs_data';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
-import { RowRenderer } from '../row_renderer';
import {
createDnsRowRenderer,
createEndgameProcessRowRenderer,
@@ -82,6 +81,9 @@ import {
EndpointAlertCriteria,
} from './generic_row_renderer';
import * as i18n from './translations';
+import { RowRenderer } from '../../../../../../../common';
+
+jest.mock('../../../../../../common/lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
index 211fa9152dc8d..c6845d7d672d2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
@@ -10,13 +10,13 @@
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
import { DnsRequestEventDetails } from '../dns/dns_request_event_details';
import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details';
import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers';
import { RegistryEventDetails } from '../registry/registry_event_details';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { SystemGenericDetails } from './generic_details';
import { SystemGenericFileDetails } from './generic_file_details';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx
index ac1e4d6748dcd..be11955169bd7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { Package } from './package';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx
index dfb9ae69ac2d4..7cff1166cd0de 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx
@@ -13,6 +13,8 @@ import '../../../../../common/mock/match_media';
import { UserHostWorkingDir } from './user_host_working_dir';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx
index 04150163fb4d4..7f0ec8b7b0b79 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx
@@ -14,6 +14,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { ZeekDetails } from './zeek_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx
index 749e450b36ae4..6b154d4d32707 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx
@@ -17,6 +17,8 @@ import '../../../../../../common/mock/match_media';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { zeekRowRenderer } from './zeek_row_renderer';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
index 7a8d284d0ec1e..2b6311b8cae83 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
@@ -10,9 +10,9 @@
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { ZeekDetails } from './zeek_details';
export const zeekRowRenderer: RowRenderer = {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx
index 61155331b1a4b..28034dac8f575 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx
@@ -28,6 +28,8 @@ import {
defaultStringRenderer,
} from './zeek_signature';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts
index e7c69b9229d70..bd05bf0656687 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts
@@ -5,15 +5,7 @@
* 2.0.
*/
-import { Direction } from '../../../../../../common/search_strategy';
-import { ColumnId } from '../column_id';
-
-/** Specifies a column's sort direction */
-export type SortDirection = 'none' | Direction;
+import { SortColumnTimeline } from '../../../../../../common/types/timeline';
/** Specifies which column the timeline is sorted on */
-export interface Sort {
- columnId: ColumnId;
- columnType: string;
- sortDirection: SortDirection;
-}
+export type Sort = SortColumnTimeline;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx
index 6af29793f9373..3e610abe79050 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx
@@ -11,8 +11,8 @@ import React from 'react';
import * as i18n from '../translations';
import { SortNumber } from './sort_number';
-import { SortDirection } from '.';
import { Direction } from '../../../../../../common/search_strategy';
+import { SortDirection } from '../../../../../../common/types/timeline';
enum SortDirectionIndicatorEnum {
SORT_UP = 'sortUp',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx
index 5ac1dcf8805cf..06d8133a24f6e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx
@@ -17,6 +17,8 @@ import { mockBrowserFields } from '../../../../common/containers/source/mock';
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock';
import { DefaultCellRenderer } from './default_cell_renderer';
+jest.mock('../../../../common/lib/kibana');
+
jest.mock('../body/renderers/get_column_renderer');
const getColumnRendererMock = getColumnRenderer as jest.Mock;
const mockImplementation = {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx
index 03e444e3a9afd..2848a850a5227 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx
@@ -5,16 +5,4 @@
* 2.0.
*/
-import { EuiDataGridCellValueElementProps } from '@elastic/eui';
-
-import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../store/timeline/model';
-
-/** The following props are provided to the function called by `renderCellValue` */
-export type CellValueElementProps = EuiDataGridCellValueElementProps & {
- data: TimelineNonEcsData[];
- eventId: string; // _id
- header: ColumnHeaderOptions;
- linkValues: string[] | undefined;
- timelineId: string;
-};
+export { CellValueElementProps } from '../../../../../common/types/timeline';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx
index 35595de646126..ef04c1177dcd6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx
@@ -11,11 +11,6 @@ import { TestProviders } from '../../../../common/mock/test_providers';
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
import { DataProviders } from '.';
-import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline';
-import { FilterManager } from '../../../../../../../../src/plugins/data/public/query/filter_manager';
-import { coreMock } from '../../../../../../../../src/core/public/mocks';
-
-const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
jest.mock('../../../../common/hooks/use_selector', () => {
const actual = jest.requireActual('../../../../common/hooks/use_selector');
@@ -25,7 +20,6 @@ jest.mock('../../../../common/hooks/use_selector', () => {
};
});
-const filterManager = new FilterManager(mockUiSettingsForFilterManager);
describe('DataProviders', () => {
const mount = useMountAppended();
@@ -33,17 +27,9 @@ describe('DataProviders', () => {
const dropMessage = ['Drop', 'query', 'build', 'here'];
test('renders correctly against snapshot', () => {
- const manageTimelineForTesting = {
- foo: {
- ...getTimelineDefaults('foo'),
- filterManager,
- },
- };
const wrapper = mount(
-
-
-
+
);
expect(wrapper.find(`[data-test-subj="dataProviders-container"]`)).toBeTruthy();
@@ -73,19 +59,10 @@ describe('DataProviders', () => {
});
describe('resizable drop target', () => {
- const manageTimelineForTesting = {
- foo: {
- ...getTimelineDefaults('test'),
- filterManager,
- },
- };
-
test('it may be resized vertically via a resize handle', () => {
const wrapper = mount(
-
-
-
+
);
@@ -98,9 +75,7 @@ describe('DataProviders', () => {
test('it never grows taller than one third (33%) of the view height', () => {
const wrapper = mount(
-
-
-
+
);
@@ -113,9 +88,7 @@ describe('DataProviders', () => {
test('it automatically displays scroll bars when the width or height of the data providers exceeds the drop target', () => {
const wrapper = mount(
-
-
-
+
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
index bdc0327026488..f642ec35d4306 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
@@ -9,19 +9,16 @@ import { rgba } from 'polished';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import uuid from 'uuid';
+import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper';
-import {
- droppableTimelineProvidersPrefix,
- IS_DRAGGING_CLASS_NAME,
-} from '../../../../common/components/drag_and_drop/helpers';
+import { droppableTimelineProvidersPrefix } from '../../../../common/components/drag_and_drop/helpers';
import { Empty } from './empty';
import { Providers } from './providers';
-import { useManageTimeline } from '../../manage_timeline';
import { timelineSelectors } from '../../../store/timeline';
import { timelineDefaults } from '../../../store/timeline/defaults';
@@ -89,11 +86,8 @@ const getDroppableId = (id: string): string =>
*/
export const DataProviders = React.memo(({ timelineId }) => {
const { browserFields } = useSourcererScope(SourcererScopeName.timeline);
- const { getManageTimelineById } = useManageTimeline();
- const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId));
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const dataProviders = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).dataProviders
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
index a3693d5ba2001..e5e5ad5f010fc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
@@ -11,7 +11,10 @@ import { useDispatch } from 'react-redux';
import { TimelineType } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
-import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
+import {
+ useDeepEqualSelector,
+ useShallowEqualSelector,
+} from '../../../../common/hooks/use_selector';
import { timelineSelectors } from '../../../store/timeline';
import { OnDataProviderEdited } from '../events';
@@ -19,7 +22,6 @@ import { ProviderBadge } from './provider_badge';
import { ProviderItemActions } from './provider_item_actions';
import { DataProvidersAnd, DataProviderType, QueryOperator } from './data_provider';
import { dragAndDropActions } from '../../../../common/store/drag_and_drop';
-import { useManageTimeline } from '../../manage_timeline';
interface ProviderItemBadgeProps {
andProviderId?: string;
@@ -75,11 +77,10 @@ export const ProviderItemBadge = React.memo(
return getTimeline(state, timelineId)?.timelineType ?? TimelineType.default;
});
- const { getManageTimelineById } = useManageTimeline();
- const isLoading = useMemo(() => getManageTimelineById(timelineId ?? '').isLoading, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, timelineId ?? '')
+ );
const togglePopover = useCallback(() => {
setIsPopoverOpen(!isPopoverOpen);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
index 7f2133aca7348..a2a91c206521a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
@@ -8,36 +8,30 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { TestProviders } from '../../../../common/mock/test_providers';
import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper';
-import { FilterManager } from '../../../../../../../../src/plugins/data/public';
import { timelineActions } from '../../../store/timeline';
import { mockDataProviders } from './mock/mock_data_providers';
import { Providers } from './providers';
import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './provider_item_actions';
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
-import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
+jest.mock('../../../../common/lib/kibana');
+
+jest.mock('../../../../common/hooks/use_selector', () => ({
+ useShallowEqualSelector: jest.fn(),
+ useDeepEqualSelector: jest.fn(),
+}));
describe('Providers', () => {
- const isLoading: boolean = true;
const mount = useMountAppended();
- const filterManager = new FilterManager(mockUiSettingsForFilterManager);
const mockOnDataProviderRemoved = jest.spyOn(timelineActions, 'removeProvider');
- const manageTimelineForTesting = {
- test: {
- ...getTimelineDefaults('test'),
- filterManager,
- isLoading,
- },
- };
-
beforeEach(() => {
jest.clearAllMocks();
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false });
});
describe('rendering', () => {
@@ -82,13 +76,12 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when the close button is clicked', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -120,13 +113,12 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const wrapper = mount(
-
-
-
-
-
+
+
+
);
wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click');
@@ -172,17 +164,16 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const mockOnToggleDataProviderEnabled = jest.spyOn(
timelineActions,
'updateDataProviderEnabled'
);
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -231,6 +222,7 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const mockOnToggleDataProviderExcluded = jest.spyOn(
timelineActions,
'updateDataProviderExcluded'
@@ -238,11 +230,9 @@ describe('Providers', () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -311,16 +301,15 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the close button is clicked', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -375,6 +364,7 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderEnabled = jest.spyOn(
@@ -384,11 +374,9 @@ describe('Providers', () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -448,6 +436,7 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderExcluded = jest.spyOn(
@@ -457,11 +446,9 @@ describe('Providers', () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
index d7436d2b891b8..5b982e4e831f7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
@@ -13,24 +13,25 @@ import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
+import {
+ DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
+ IS_DRAGGING_CLASS_NAME,
+} from '@kbn/securitysolution-t-grid';
import { timelineActions } from '../../../store/timeline';
import { AndOrBadge } from '../../../../common/components/and_or_badge';
-import { useDraggableKeyboardWrapper } from '../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { AddDataProviderPopover } from './add_data_provider_popover';
import { BrowserFields } from '../../../../common/containers/source';
import {
- DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
getTimelineProviderDraggableId,
getTimelineProviderDroppableId,
- IS_DRAGGING_CLASS_NAME,
} from '../../../../common/components/drag_and_drop/helpers';
-
import { DataProvider, DataProviderType, DataProvidersAnd, IS_OPERATOR } from './data_provider';
import { EMPTY_GROUP, flattenIntoAndGroups } from './helpers';
import { ProviderItemBadge } from './provider_item_badge';
import * as i18n from './translations';
+import { useKibana } from '../../../../common/lib/kibana';
export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group';
@@ -159,6 +160,7 @@ export const DataProvidersGroupItem = React.memo(
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [, setClosePopOverTrigger] = useState(false);
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
@@ -244,7 +246,7 @@ export const DataProvidersGroupItem = React.memo(
setIsPopoverOpen(true);
}, []);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId,
fieldName: dataProvider.queryMatch.field,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
index e13bed1e2eff6..5f08bf5a016f5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
@@ -22,6 +22,7 @@ import { useTimelineEvents } from '../../../containers/index';
import { useTimelineEventsDetails } from '../../../containers/details/index';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
+import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components';
jest.mock('../../../containers/index', () => ({
useTimelineEvents: jest.fn(),
@@ -57,6 +58,10 @@ jest.mock('../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
+ timelines: {
+ getLastUpdated: jest.fn(),
+ getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
+ },
},
}),
useGetUserSavedObjectPermissions: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
index bb2a995ff9fae..b67b9348f51aa 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
@@ -17,7 +17,7 @@ import { isEmpty } from 'lodash/fp';
import React, { useEffect, useCallback } from 'react';
import styled from 'styled-components';
import { Dispatch } from 'redux';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { InPortal } from 'react-reverse-portal';
@@ -27,12 +27,17 @@ import { TimelineItem } from '../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../containers/index';
import { defaultHeaders } from '../body/column_headers/default_headers';
import { StatefulBody } from '../body';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { Footer, footerHeight } from '../footer';
import { calculateTotalPages } from '../helpers';
import { TimelineRefetch } from '../refetch_timeline';
-import { useManageTimeline } from '../../manage_timeline';
-import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
+import {
+ ControlColumnProps,
+ RowRenderer,
+ TimelineEventsType,
+ TimelineId,
+ TimelineTabs,
+ ToggleDetailPanel,
+} from '../../../../../common/types/timeline';
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
@@ -48,10 +53,9 @@ import { TimelineModel } from '../../../../timelines/store/timeline/model';
import { TimelineDatePickerLock } from '../date_picker_lock';
import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen';
import { activeTimeline } from '../../../containers/active_timeline_context';
-import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { DetailsPanel } from '../../side_panel';
import { EqlQueryBarTimeline } from '../query_bar/eql';
-import { defaultControlColumn, ControlColumnProps } from '../body/control_columns';
+import { defaultControlColumn } from '../body/control_columns';
import { Sort } from '../body/sort';
const TimelineHeaderContainer = styled.div`
@@ -166,6 +170,7 @@ export const EqlTabContentComponent: React.FC = ({
timerangeKind,
updateEventTypeAndIndexesName,
}) => {
+ const dispatch = useDispatch();
const { query: eqlQuery = '', ...restEqlOption } = eqlOptions;
const { portalNode: eqlEventsCountPortalNode } = useEqlEventsCountPortal();
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
@@ -192,12 +197,13 @@ export const EqlTabContentComponent: React.FC = ({
return [...columnFields, ...requiredFieldsForActions];
};
- const { initializeTimeline, setIsTimelineLoading } = useManageTimeline();
useEffect(() => {
- initializeTimeline({
- id: timelineId,
- });
- }, [initializeTimeline, timelineId]);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ id: timelineId,
+ })
+ );
+ }, [dispatch, timelineId]);
const [
isQueryLoading,
@@ -230,8 +236,13 @@ export const EqlTabContentComponent: React.FC = ({
}, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]);
useEffect(() => {
- setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer });
- }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]);
+ dispatch(
+ timelineActions.updateIsLoading({
+ id: timelineId,
+ isLoading: isQueryLoading || loadingSourcerer,
+ })
+ );
+ }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
@@ -385,7 +396,6 @@ const makeMapStateToProps = () => {
};
return mapStateToProps;
};
-
const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
updateEventTypeAndIndexesName: (newEventType: TimelineEventsType, newIndexNames: string[]) => {
dispatch(timelineActions.updateEventType({ id: timelineId, eventType: newEventType }));
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
index 21e213b799535..ca7c3596d13bb 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
@@ -5,10 +5,20 @@
* 2.0.
*/
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { ColumnId } from './body/column_id';
-import { SortDirection } from './body/sort';
import { DataProvider, QueryOperator } from './data_providers/data_provider';
+export {
+ OnColumnSorted,
+ OnColumnsSorted,
+ OnColumnRemoved,
+ OnColumnResized,
+ OnChangePage,
+ OnPinEvent,
+ OnRowSelected,
+ OnSelectAll,
+ OnUnPinEvent,
+ OnUpdateColumns,
+} from '../../../../common/types/timeline';
export type OnDataProviderEdited = ({
andProviderId,
@@ -35,38 +45,3 @@ export type OnRangeSelected = (range: string) => void;
/** Invoked when a user updates a column's filter */
export type OnFilterChange = (filter: { columnId: ColumnId; filter: string }) => void;
-
-/** Invoked when a column is sorted */
-export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void;
-
-export type OnColumnsSorted = (
- sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }>
-) => void;
-
-export type OnColumnRemoved = (columnId: ColumnId) => void;
-
-export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void;
-
-/** Invoked when a user clicks to load more item */
-export type OnChangePage = (nextPage: number) => void;
-
-/** Invoked when a user pins an event */
-export type OnPinEvent = (eventId: string) => void;
-
-/** Invoked when a user checks/un-checks a row */
-export type OnRowSelected = ({
- eventIds,
- isSelected,
-}: {
- eventIds: string[];
- isSelected: boolean;
-}) => void;
-
-/** Invoked when a user checks/un-checks the select all checkbox */
-export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
-
-/** Invoked when columns are updated */
-export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void;
-
-/** Invoked when a user unpins an event */
-export type OnUnPinEvent = (eventId: string) => void;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx
index f0a14e990e1cc..cf8d51546a899 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx
@@ -12,6 +12,8 @@ import { TestProviders } from '../../../../common/mock/test_providers';
import { FooterComponent, PagingControlComponent } from './index';
+jest.mock('../../../../common/lib/kibana');
+
describe('Footer Timeline Component', () => {
const loadMore = jest.fn();
const updatedAt = 1546878704036;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
index 4c5432f686c93..ac6f6e52db1e2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
@@ -24,15 +24,14 @@ import React, { FC, useCallback, useEffect, useState, useMemo } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
-import { LoadingPanel } from '../../loading';
import { OnChangePage } from '../events';
import { EVENTS_COUNT_BUTTON_CLASS_NAME } from '../helpers';
import * as i18n from './translations';
import { useEventDetailsWidthContext } from '../../../../common/components/events_viewer/event_details_width_context';
-import { useManageTimeline } from '../../manage_timeline';
-import { LastUpdatedAt } from '../../../../common/components/last_updated';
-import { timelineActions } from '../../../store/timeline';
+import { timelineActions, timelineSelectors } from '../../../store/timeline';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { useKibana } from '../../../../common/lib/kibana';
export const isCompactFooter = (width: number): boolean => width < 600;
@@ -42,12 +41,13 @@ interface FixedWidthLastUpdatedContainerProps {
const FixedWidthLastUpdatedContainer = React.memo(
({ updatedAt }) => {
+ const { timelines } = useKibana().services;
const width = useEventDetailsWidthContext();
const compact = useMemo(() => isCompactFooter(width), [width]);
return (
-
+ {timelines.getLastUpdated({ updatedAt, compact })}
);
}
@@ -259,14 +259,16 @@ export const FooterComponent = ({
totalCount,
}: FooterProps) => {
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [paginationLoading, setPaginationLoading] = useState(false);
- const { getManageTimelineById } = useManageTimeline();
- const { documentType, loadingText, footerText } = useMemo(() => getManageTimelineById(id), [
- getManageTimelineById,
- id,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const {
+ documentType = i18n.TOTAL_COUNT_OF_EVENTS,
+ loadingText = i18n.LOADING_EVENTS,
+ footerText = i18n.TOTAL_COUNT_OF_EVENTS,
+ } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const handleChangePageClick = useCallback(
(nextPage: number) => {
@@ -322,13 +324,13 @@ export const FooterComponent = ({
if (isLoading && !paginationLoading) {
return (
-
+ {timelines.getLoadingPanel({
+ dataTestSubj: 'LoadingPanelTimeline',
+ height: '35px',
+ showBorder: false,
+ text: loadingText,
+ width: '100%',
+ })}
);
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
index fa8a8b743646d..6736573cac293 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
@@ -43,3 +43,10 @@ export const AUTO_REFRESH_ACTIVE = i18n.translate(
defaultMessage: 'Auto-Refresh Active',
}
);
+
+export const LOADING_EVENTS = i18n.translate(
+ 'xpack.securitySolution.footer.loadingEventsDataLabel',
+ {
+ defaultMessage: 'Loading Events',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
index 0093ce2f95bdd..f2a4071111602 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
@@ -14,7 +14,7 @@ import {
getFocusedAriaColindexCell,
getTableSkipFocus,
stopPropagationAndPreventDefault,
-} from '../../../common/components/accessibility/helpers';
+} from '../../../../../timelines/public';
import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury';
import {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
index 5e86bf8d75385..e95efdf754418 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
@@ -11,16 +11,15 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { isTab } from '../../../../../timelines/public';
import { timelineActions, timelineSelectors } from '../../store/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { defaultHeaders } from './body/column_headers/default_headers';
-import { RowRenderer } from './body/renderers/row_renderer';
import { CellValueElementProps } from './cell_rendering';
-import { isTab } from '../../../common/components/accessibility/helpers';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header';
-import { TimelineType, TimelineId } from '../../../../common/types/timeline';
+import { TimelineType, TimelineId, RowRenderer } from '../../../../common/types/timeline';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { activeTimeline } from '../../containers/active_timeline_context';
import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
index 0f781b0958d02..f4d5570ce40d3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
@@ -23,6 +23,7 @@ import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
import { PinnedTabContentComponent, Props as PinnedTabContentComponentProps } from '.';
import { Direction } from '../../../../../common/search_strategy';
+import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components';
jest.mock('../../../containers/index', () => ({
useTimelineEvents: jest.fn(),
@@ -57,6 +58,10 @@ jest.mock('../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
+ timelines: {
+ getLastUpdated: jest.fn(),
+ getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
+ },
},
}),
useGetUserSavedObjectPermissions: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx
index c01cf5c8aa0f0..b5e3d853bc81c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx
@@ -19,7 +19,6 @@ import { Direction } from '../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../containers/index';
import { defaultHeaders } from '../body/column_headers/default_headers';
import { StatefulBody } from '../body';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { Footer, footerHeight } from '../footer';
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
@@ -29,14 +28,18 @@ import { timelineDefaults } from '../../../store/timeline/defaults';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen';
import { TimelineModel } from '../../../store/timeline/model';
-import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { State } from '../../../../common/store';
import { calculateTotalPages } from '../helpers';
-import { TimelineTabs } from '../../../../../common/types/timeline';
+import {
+ ControlColumnProps,
+ RowRenderer,
+ TimelineTabs,
+ ToggleDetailPanel,
+} from '../../../../../common/types/timeline';
import { DetailsPanel } from '../../side_panel';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
-import { defaultControlColumn, ControlColumnProps } from '../body/control_columns';
+import { defaultControlColumn } from '../body/control_columns';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
overflow-y: hidden;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx
index 8790d8c98c161..b2b304e16c4a0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx
@@ -22,7 +22,6 @@ import {
SavedQueryTimeFilter,
} from '../../../../../../../../src/plugins/data/public';
import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury';
-import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store';
import { KqlMode } from '../../../../timelines/store/timeline/model';
import { useSavedQueryServices } from '../../../../common/utils/saved_query_services';
import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker';
@@ -30,6 +29,7 @@ import { QueryBar } from '../../../../common/components/query_bar';
import { DataProvider } from '../data_providers/data_provider';
import { buildGlobalQuery } from '../helpers';
import { timelineActions } from '../../../store/timeline';
+import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../../common/types/timeline';
export interface QueryBarTimelineComponentProps {
dataProviders: DataProvider[];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
index acae8c8c53cd0..9bf7ee28f3934 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
@@ -59,6 +59,15 @@ jest.mock('../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
+ timelines: {
+ getLastUpdated: jest.fn(),
+ getLoadingPanel: jest.fn(),
+ getUseDraggableKeyboardWrapper: () =>
+ jest.fn().mockReturnValue({
+ onBlur: jest.fn(),
+ onKeyDown: jest.fn(),
+ }),
+ },
},
}),
useGetUserSavedObjectPermissions: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
index 4298f2ff74517..6f0bbd026cd7b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
@@ -17,12 +17,11 @@ import { isEmpty } from 'lodash/fp';
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import styled from 'styled-components';
import { Dispatch } from 'redux';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { InPortal } from 'react-reverse-portal';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { CellValueElementProps } from '../cell_rendering';
import { Direction, TimelineItem } from '../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../containers/index';
@@ -34,18 +33,20 @@ import { TimelineHeader } from '../header';
import { calculateTotalPages, combineQueries } from '../helpers';
import { TimelineRefetch } from '../refetch_timeline';
import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public';
-import { useManageTimeline } from '../../manage_timeline';
-import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
+import {
+ ControlColumnProps,
+ KueryFilterQueryKind,
+ RowRenderer,
+ TimelineEventsType,
+ TimelineId,
+ TimelineTabs,
+ ToggleDetailPanel,
+} from '../../../../../common/types/timeline';
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
import { PickEventType } from '../search_or_filter/pick_events';
-import {
- inputsModel,
- inputsSelectors,
- KueryFilterQueryKind,
- State,
-} from '../../../../common/store';
+import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { sourcererActions } from '../../../../common/store/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
@@ -55,10 +56,9 @@ import { TimelineModel } from '../../../../timelines/store/timeline/model';
import { TimelineDatePickerLock } from '../date_picker_lock';
import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen';
import { activeTimeline } from '../../../containers/active_timeline_context';
-import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { DetailsPanel } from '../../side_panel';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
-import { defaultControlColumn, ControlColumnProps } from '../body/control_columns';
+import { defaultControlColumn } from '../body/control_columns';
const TimelineHeaderContainer = styled.div`
margin-top: 6px;
@@ -180,6 +180,7 @@ export const QueryTabContentComponent: React.FC = ({
timerangeKind,
updateEventTypeAndIndexesName,
}) => {
+ const dispatch = useDispatch();
const { portalNode: timelineEventsCountPortalNode } = useTimelineEventsCountPortal();
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
const {
@@ -231,13 +232,14 @@ export const QueryTabContentComponent: React.FC = ({
type: columnType,
}));
- const { initializeTimeline, setIsTimelineLoading } = useManageTimeline();
useEffect(() => {
- initializeTimeline({
- filterManager,
- id: timelineId,
- });
- }, [initializeTimeline, filterManager, timelineId]);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ filterManager,
+ id: timelineId,
+ })
+ );
+ }, [filterManager, timelineId, dispatch]);
const [
isQueryLoading,
@@ -270,8 +272,13 @@ export const QueryTabContentComponent: React.FC = ({
}, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]);
useEffect(() => {
- setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer });
- }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]);
+ dispatch(
+ timelineActions.updateIsLoading({
+ id: timelineId,
+ isLoading: isQueryLoading || loadingSourcerer,
+ })
+ );
+ }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx
index 4ea4f94abff63..33ab2e0049828 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx
@@ -12,17 +12,13 @@ import { Dispatch } from 'redux';
import deepEqual from 'fast-deep-equal';
import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public';
-import {
- SerializedFilterQuery,
- State,
- inputsModel,
- inputsSelectors,
-} from '../../../../common/store';
+import { State, inputsModel, inputsSelectors } from '../../../../common/store';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { KqlMode, TimelineModel } from '../../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
import { dispatchUpdateReduxTime } from '../../../../common/components/super_date_picker';
import { SearchOrFilter } from './search_or_filter';
+import { SerializedFilterQuery } from '../../../../../common/types/timeline';
interface OwnProps {
filterManager: FilterManager;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
index 262709ed98e5a..f1c4b7c3ef089 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
@@ -10,9 +10,9 @@ import React, { useCallback } from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public';
-import { KueryFilterQuery } from '../../../../common/store';
import { KqlMode } from '../../../../timelines/store/timeline/model';
import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker';
+import { KueryFilterQuery } from '../../../../../common/types/timeline';
import { DataProvider } from '../data_providers/data_provider';
import { QueryBarTimeline } from '../query_bar';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
index 0c584a2d62efe..3514766b334a0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
@@ -8,9 +8,9 @@
import { EuiLoadingSpinner } from '@elastic/eui';
import { rgba } from 'polished';
import styled, { createGlobalStyle } from 'styled-components';
+import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { TimelineEventsType } from '../../../../common/types/timeline';
-import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/components/drag_and_drop/helpers';
import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers';
import { EVENTS_TABLE_ARIA_LABEL } from './translations';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
index adaa5f98c88c4..8cdd7722d7fbd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
@@ -10,7 +10,12 @@ import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 're
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import { TimelineTabs, TimelineId, TimelineType } from '../../../../../common/types/timeline';
+import {
+ RowRenderer,
+ TimelineTabs,
+ TimelineId,
+ TimelineType,
+} from '../../../../../common/types/timeline';
import {
useShallowEqualSelector,
useDeepEqualSelector,
@@ -20,7 +25,6 @@ import {
TimelineEventsCountBadge,
} from '../../../../common/hooks/use_timeline_events_count';
import { timelineActions } from '../../../store/timeline';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { CellValueElementProps } from '../cell_rendering';
import {
getActiveTabSelector,
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx
index 37fdd5a444b2b..86624ba161a83 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx
@@ -69,7 +69,7 @@ export const useTimelineEventsDetails = ({
.search(
request,
{
- strategy: 'securitySolutionTimelineSearchStrategy',
+ strategy: 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
}
)
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
index 17c107899d85a..00df0146e06d5 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
@@ -14,7 +14,7 @@ import { Subscription } from 'rxjs';
import { ESQuery } from '../../../common/typed_json';
import { isCompleteResponse, isErrorResponse } from '../../../../../../src/plugins/data/public';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
-import { inputsModel, KueryFilterQueryKind } from '../../common/store';
+import { inputsModel } from '../../common/store';
import { useKibana } from '../../common/lib/kibana';
import { createFilter } from '../../common/containers/helpers';
import { timelineActions } from '../../timelines/store/timeline';
@@ -33,7 +33,7 @@ import {
} from '../../../common/search_strategy';
import { InspectResponse } from '../../types';
import * as i18n from './translations';
-import { TimelineId } from '../../../common/types/timeline';
+import { KueryFilterQueryKind, TimelineId } from '../../../common/types/timeline';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
import { activeTimeline } from './active_timeline_context';
import {
@@ -214,9 +214,7 @@ export const useTimelineEvents = ({
searchSubscription$.current = data.search
.search, TimelineResponse>(request, {
strategy:
- request.language === 'eql'
- ? 'securitySolutionTimelineEqlSearchStrategy'
- : 'securitySolutionTimelineSearchStrategy',
+ request.language === 'eql' ? 'timelineEqlSearchStrategy' : 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx
index 4a6eab13ba4f1..be93a13ab1c6a 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx
@@ -64,7 +64,7 @@ export const useTimelineKpis = ({
searchSubscription$.current = data.search
.search(request, {
- strategy: 'securitySolutionTimelineSearchStrategy',
+ strategy: 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx
index 38eb6d3d222f8..99f45c7d9a4b4 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx
@@ -9,8 +9,8 @@ import { isEmpty } from 'lodash/fp';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
import { TimelinesStorage } from './types';
import { useKibana } from '../../../common/lib/kibana';
-import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model';
-import { TimelineIdLiteral } from '../../../../common/types/timeline';
+import { TimelineModel } from '../../store/timeline/model';
+import { ColumnHeaderOptions, TimelineIdLiteral } from '../../../../common/types/timeline';
export const LOCAL_STORAGE_TIMELINE_KEY = 'timelines';
const EMPTY_TIMELINE = {} as {
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
index fe8650da7a090..5d2e45b638d59 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
@@ -12,7 +12,7 @@ import { useParams } from 'react-router-dom';
import { TimelineId, TimelineType } from '../../../common/types/timeline';
import { HeaderPage } from '../../common/components/header_page';
-import { WrapperPage } from '../../common/components/wrapper_page';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useKibana } from '../../common/lib/kibana';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { OverviewEmpty } from '../../overview/components/overview_empty';
@@ -45,8 +45,8 @@ export const TimelinesPageComponent: React.FC = () => {
<>
{indicesExist ? (
<>
-
-
+
+
{capabilitiesCanUserCRUD && (
@@ -89,13 +89,13 @@ export const TimelinesPageComponent: React.FC = () => {
data-test-subj="stateful-open-timeline"
/>
-
+
>
) : (
-
+
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
index 11e9a625d05d0..a3429c9247ffd 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
@@ -8,25 +8,42 @@
import actionCreatorFactory from 'typescript-fsa';
import { Filter } from '../../../../../../../src/plugins/data/public';
-import { Sort } from '../../../timelines/components/timeline/body/sort';
import {
DataProvider,
DataProviderType,
QueryOperator,
} from '../../../timelines/components/timeline/data_providers/data_provider';
-import { SerializedFilterQuery } from '../../../common/store/types';
-import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
-import { FieldsEqlOptions, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
+import { KqlMode, TimelineModel } from './model';
+import { FieldsEqlOptions } from '../../../../common/search_strategy/timeline';
import {
TimelineEventsType,
- TimelineExpandedDetail,
- TimelineExpandedDetailType,
- TimelineTypeLiteral,
RowRendererId,
TimelineTabs,
+ TimelinePersistInput,
+ SerializedFilterQuery,
} from '../../../../common/types/timeline';
import { InsertTimeline } from './types';
+import { tGridActions } from '../../../../../timelines/public';
+export const {
+ applyDeltaToColumnWidth,
+ clearEventsDeleted,
+ clearEventsLoading,
+ clearSelected,
+ initializeTGridSettings,
+ removeColumn,
+ setEventsDeleted,
+ setEventsLoading,
+ setSelected,
+ setTGridSelectAll,
+ toggleDetailPanel,
+ updateColumns,
+ updateIsLoading,
+ updateItemsPerPage,
+ updateItemsPerPageOptions,
+ updateSort,
+ upsertColumn,
+} = tGridActions;
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline');
@@ -38,62 +55,14 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI
'ADD_NOTE_TO_EVENT'
);
-export type ToggleDetailPanel = TimelineExpandedDetailType & {
- tabType?: TimelineTabs;
- timelineId: string;
-};
-
-export const toggleDetailPanel = actionCreator('TOGGLE_DETAIL_PANEL');
-
-export const upsertColumn = actionCreator<{
- column: ColumnHeaderOptions;
- id: string;
- index: number;
-}>('UPSERT_COLUMN');
-
export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER');
-export const applyDeltaToColumnWidth = actionCreator<{
- id: string;
- columnId: string;
- delta: number;
-}>('APPLY_DELTA_TO_COLUMN_WIDTH');
-
-export interface TimelineInput {
- id: string;
- dataProviders?: DataProvider[];
- dateRange?: {
- start: string;
- end: string;
- };
- excludedRowRendererIds?: RowRendererId[];
- expandedDetail?: TimelineExpandedDetail;
- filters?: Filter[];
- columns: ColumnHeaderOptions[];
- itemsPerPage?: number;
- indexNames: string[];
- kqlQuery?: {
- filterQuery: SerializedFilterQuery | null;
- };
- show?: boolean;
- sort?: Sort[];
- showCheckboxes?: boolean;
- timelineType?: TimelineTypeLiteral;
- templateTimelineId?: string | null;
- templateTimelineVersion?: number | null;
-}
-
-export const saveTimeline = actionCreator('SAVE_TIMELINE');
+export const saveTimeline = actionCreator('SAVE_TIMELINE');
-export const createTimeline = actionCreator('CREATE_TIMELINE');
+export const createTimeline = actionCreator('CREATE_TIMELINE');
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
-export const removeColumn = actionCreator<{
- id: string;
- columnId: string;
-}>('REMOVE_COLUMN');
-
export const removeProvider = actionCreator<{
id: string;
providerId: string;
@@ -129,16 +98,6 @@ export const endTimelineSaving = actionCreator<{
id: string;
}>('END_TIMELINE_SAVING');
-export const updateIsLoading = actionCreator<{
- id: string;
- isLoading: boolean;
-}>('UPDATE_LOADING');
-
-export const updateColumns = actionCreator<{
- id: string;
- columns: ColumnHeaderOptions[];
-}>('UPDATE_COLUMNS');
-
export const updateDataProviderEnabled = actionCreator<{
id: string;
enabled: boolean;
@@ -189,15 +148,6 @@ export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean
export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE');
-export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>(
- 'UPDATE_ITEMS_PER_PAGE'
-);
-
-export const updateItemsPerPageOptions = actionCreator<{
- id: string;
- itemsPerPageOptions: number[];
-}>('UPDATE_ITEMS_PER_PAGE_OPTIONS');
-
export const updateTitleAndDescription = actionCreator<{
description: string;
id: string;
@@ -216,8 +166,6 @@ export const updateRange = actionCreator<{ id: string; start: string; end: strin
'UPDATE_RANGE'
);
-export const updateSort = actionCreator<{ id: string; sort: Sort[] }>('UPDATE_SORT');
-
export const updateAutoSaveMsg = actionCreator<{
timelineId: string | null;
newTimelineModel: TimelineModel | null;
@@ -235,37 +183,6 @@ export const setFilters = actionCreator<{
filters: Filter[];
}>('SET_TIMELINE_FILTERS');
-export const setSelected = actionCreator<{
- id: string;
- eventIds: Readonly>;
- isSelected: boolean;
- isSelectAllChecked: boolean;
-}>('SET_TIMELINE_SELECTED');
-
-export const clearSelected = actionCreator<{
- id: string;
-}>('CLEAR_TIMELINE_SELECTED');
-
-export const setEventsLoading = actionCreator<{
- id: string;
- eventIds: string[];
- isLoading: boolean;
-}>('SET_TIMELINE_EVENTS_LOADING');
-
-export const clearEventsLoading = actionCreator<{
- id: string;
-}>('CLEAR_TIMELINE_EVENTS_LOADING');
-
-export const setEventsDeleted = actionCreator<{
- id: string;
- eventIds: string[];
- isDeleted: boolean;
-}>('SET_TIMELINE_EVENTS_DELETED');
-
-export const clearEventsDeleted = actionCreator<{
- id: string;
-}>('CLEAR_TIMELINE_EVENTS_DELETED');
-
export const updateEventType = actionCreator<{ id: string; eventType: TimelineEventsType }>(
'UPDATE_EVENT_TYPE'
);
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
index 7e76f6035f8b5..d8fd82005dfbe 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
@@ -10,7 +10,6 @@ import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/t
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range';
import { SubsetTimelineModel, TimelineModel } from './model';
-import { Direction } from '../../../../common/search_strategy';
// normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false
const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false);
@@ -66,7 +65,7 @@ export const timelineDefaults: SubsetTimelineModel &
{
columnId: '@timestamp',
columnType: 'number',
- sortDirection: Direction.desc,
+ sortDirection: 'desc',
},
],
status: TimelineStatus.draft,
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
index 5f5d76990b5ff..8f2631dac6769 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
@@ -41,6 +41,7 @@ import {
TimelineType,
ResponseTimeline,
TimelineResult,
+ ColumnHeaderOptions,
} from '../../../../common/types/timeline';
import { inputsModel } from '../../../common/store/inputs';
import { addError } from '../../../common/store/app/actions';
@@ -81,7 +82,7 @@ import {
showCallOutUnauthorizedMsg,
saveTimeline,
} from './actions';
-import { ColumnHeaderOptions, TimelineModel } from './model';
+import { TimelineModel } from './model';
import { epicPersistNote, timelineNoteActionsType } from './epic_note';
import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_pinned_event';
import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite';
@@ -96,13 +97,11 @@ const timelineActionsType = [
addProvider.type,
addTimeline.type,
dataProviderEdited.type,
- removeColumn.type,
removeProvider.type,
saveTimeline.type,
setExcludedRowRendererIds.type,
setFilters.type,
setSavedQueryId.type,
- updateColumns.type,
updateDataProviderEnabled.type,
updateDataProviderExcluded.type,
updateDataProviderKqlQuery.type,
@@ -110,10 +109,13 @@ const timelineActionsType = [
updateEqlOptions.type,
updateEventType.type,
updateKqlMode.type,
- updateIndexNames.type,
updateProviders.type,
- updateSort.type,
updateTitleAndDescription.type,
+
+ updateIndexNames.type,
+ removeColumn.type,
+ updateColumns.type,
+ updateSort.type,
updateRange.type,
upsertColumn.type,
];
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
index 2172cf8562c97..610c394614c32 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
@@ -8,7 +8,6 @@
import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
import uuid from 'uuid';
-import { ToggleDetailPanel } from './actions';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { Sort } from '../../../timelines/components/timeline/body/sort';
@@ -20,22 +19,24 @@ import {
IS_OPERATOR,
EXISTS_OPERATOR,
} from '../../../timelines/components/timeline/data_providers/data_provider';
-import { SerializedFilterQuery } from '../../../common/store/model';
import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import {
+ ColumnHeaderOptions,
TimelineEventsType,
- TimelineExpandedDetail,
TimelineTypeLiteral,
TimelineType,
RowRendererId,
TimelineStatus,
TimelineId,
TimelineTabs,
+ SerializedFilterQuery,
+ ToggleDetailPanel,
+ TimelinePersistInput,
} from '../../../../common/types/timeline';
import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range';
import { timelineDefaults } from './defaults';
-import { ColumnHeaderOptions, KqlMode, TimelineModel } from './model';
+import { KqlMode, TimelineModel } from './model';
import { TimelineById } from './types';
import {
DEFAULT_FROM_MOMENT,
@@ -168,47 +169,20 @@ export const addTimelineToStore = ({
};
};
-interface AddNewTimelineParams {
- columns: ColumnHeaderOptions[];
- dataProviders?: DataProvider[];
- dateRange?: {
- start: string;
- end: string;
- };
- excludedRowRendererIds?: RowRendererId[];
- expandedDetail?: TimelineExpandedDetail;
- filters?: Filter[];
- id: string;
- itemsPerPage?: number;
- indexNames: string[];
- kqlQuery?: {
- filterQuery: SerializedFilterQuery | null;
- };
- show?: boolean;
- sort?: Sort[];
- showCheckboxes?: boolean;
+interface AddNewTimelineParams extends TimelinePersistInput {
timelineById: TimelineById;
timelineType: TimelineTypeLiteral;
}
/** Adds a new `Timeline` to the provided collection of `TimelineById` */
export const addNewTimeline = ({
- columns,
- dataProviders = [],
- dateRange: maybeDateRange,
- excludedRowRendererIds = [],
- expandedDetail = {},
- filters = timelineDefaults.filters,
id,
- itemsPerPage = timelineDefaults.itemsPerPage,
- indexNames,
- kqlQuery = { filterQuery: null },
- sort = timelineDefaults.sort,
- show = false,
- showCheckboxes = false,
timelineById,
timelineType,
+ dateRange: maybeDateRange,
+ ...timelineProps
}: AddNewTimelineParams): TimelineById => {
+ const timeline = timelineById[id];
const { from: startDateRange, to: endDateRange } = normalizeTimeRange({ from: '', to: '' });
const dateRange = maybeDateRange ?? { start: startDateRange, end: endDateRange };
const templateTimelineInfo =
@@ -222,23 +196,14 @@ export const addNewTimeline = ({
...timelineById,
[id]: {
id,
+ ...(timeline ? timeline : {}),
...timelineDefaults,
- columns,
- dataProviders,
+ ...timelineProps,
dateRange,
- expandedDetail,
- excludedRowRendererIds,
- filters,
- itemsPerPage,
- indexNames,
- kqlQuery,
- sort,
- show,
savedObjectId: null,
version: null,
isSaving: false,
isLoading: false,
- showCheckboxes,
timelineType,
...templateTimelineInfo,
},
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
index 559cec57dd55c..a68617536c6af 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
@@ -5,63 +5,29 @@
* 2.0.
*/
-import { EuiDataGridColumn } from '@elastic/eui';
-
-import { Filter, IFieldSubType } from '../../../../../../../src/plugins/data/public';
-
import { DataProvider } from '../../components/timeline/data_providers/data_provider';
-import { Sort } from '../../components/timeline/body/sort';
-import {
- EqlOptionsSelected,
- TimelineNonEcsData,
-} from '../../../../common/search_strategy/timeline';
-import { SerializedFilterQuery } from '../../../common/store/types';
+import { EqlOptionsSelected } from '../../../../common/search_strategy/timeline';
import type {
TimelineEventsType,
- TimelineExpandedDetail,
TimelineType,
TimelineStatus,
- RowRendererId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { PinnedEvent } from '../../../../common/types/timeline/pinned_event';
+import type { TGridModelForTimeline } from '../../../../../timelines/public';
export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages
export type KqlMode = 'filter' | 'search';
export type ColumnHeaderType = 'not-filtered' | 'text-filter';
-/** Uniquely identifies a column */
-export type ColumnId = string;
-
-/** The specification of a column header */
-export type ColumnHeaderOptions = Pick<
- EuiDataGridColumn,
- 'display' | 'displayAsText' | 'id' | 'initialWidth'
-> & {
- aggregatable?: boolean;
- category?: string;
- columnHeaderType: ColumnHeaderType;
- description?: string;
- example?: string;
- format?: string;
- linkField?: string;
- placeholder?: string;
- subType?: IFieldSubType;
- type?: string;
-};
-
-export interface TimelineModel {
+export type TimelineModel = TGridModelForTimeline & {
/** The selected tab to displayed in the timeline */
activeTab: TimelineTabs;
prevActiveTab: TimelineTabs;
- /** The columns displayed in the timeline */
- columns: ColumnHeaderOptions[];
/** Timeline saved object owner */
createdBy?: string;
/** The sources of the event data shown in the timeline */
dataProviders: DataProvider[];
- /** Events to not be rendered **/
- deletedEventIds: string[];
/** A summary of the events and notes in this timeline */
description: string;
eqlOptions: EqlOptionsSelected;
@@ -69,40 +35,16 @@ export interface TimelineModel {
eventType?: TimelineEventsType;
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
eventIdToNoteIds: Record;
- /** A list of Ids of excluded Row Renderers */
- excludedRowRendererIds: RowRendererId[];
- /** This holds the view information for the flyout when viewing timeline in a consuming view (i.e. hosts page) or the side panel in the primary timeline view */
- expandedDetail: TimelineExpandedDetail;
- filters?: Filter[];
- /** When non-empty, display a graph view for this event */
- graphEventId?: string;
/** The chronological history of actions related to this timeline */
historyIds: string[];
/** The chronological history of actions related to this timeline */
highlightedDropAndProviderId: string;
- /** Uniquely identifies the timeline */
- id: string;
- /** TO DO sourcerer @X define this */
- indexNames: string[];
- /** If selectAll checkbox in header is checked **/
- isSelectAllChecked: boolean;
- /** Events to be rendered as loading **/
- loadingEventIds: string[];
- savedObjectId: string | null;
/** When true, this timeline was marked as "favorite" by the user */
isFavorite: boolean;
/** When true, the timeline will update as new data arrives */
isLive: boolean;
- /** The number of items to show in a single page of results */
- itemsPerPage: number;
- /** Displays a series of choices that when selected, become the value of `itemsPerPage` */
- itemsPerPageOptions: number[];
/** determines the behavior of the KQL bar */
kqlMode: KqlMode;
- /** the KQL query in the KQL bar */
- kqlQuery: {
- filterQuery: SerializedFilterQuery | null;
- };
/** Title */
title: string;
/** timelineType: default | template */
@@ -116,30 +58,18 @@ export interface TimelineModel {
/** Events pinned to this timeline */
pinnedEventIds: Record;
pinnedEventsSaveObject: Record;
- /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */
- dateRange: {
- start: string;
- end: string;
- };
showSaveModal?: boolean;
savedQueryId?: string | null;
- /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/
- selectedEventIds: Record;
/** When true, show the timeline flyover */
show: boolean;
- /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/
- showCheckboxes: boolean;
- /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
- sort: Sort[];
/** status: active | draft */
status: TimelineStatus;
/** updated saved object timestamp */
updated?: number;
/** timeline is saving */
isSaving: boolean;
- isLoading: boolean;
version: string | null;
-}
+};
export type SubsetTimelineModel = Readonly<
Pick<
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
index 1c65c01a0bdfc..8a5c8546d3834 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
@@ -7,6 +7,7 @@
import { cloneDeep } from 'lodash/fp';
import {
+ ColumnHeaderOptions,
TimelineType,
TimelineStatus,
TimelineTabs,
@@ -47,7 +48,7 @@ import {
upsertTimelineColumn,
updateGraphEventId,
} from './helpers';
-import { ColumnHeaderOptions, TimelineModel } from './model';
+import { TimelineModel } from './model';
import { timelineDefaults } from './defaults';
import { TimelineById } from './types';
import { Direction } from '../../../../common/search_strategy';
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 80c6d83075719..656784c330e45 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -13,32 +13,22 @@ import {
addNoteToEvent,
addProvider,
addTimeline,
- applyDeltaToColumnWidth,
applyKqlFilterQuery,
- clearEventsDeleted,
- clearEventsLoading,
- clearSelected,
createTimeline,
dataProviderEdited,
endTimelineSaving,
pinEvent,
- removeColumn,
removeProvider,
- setEventsDeleted,
setActiveTabTimeline,
- setEventsLoading,
setExcludedRowRendererIds,
setFilters,
setInsertTimeline,
setSavedQueryId,
- setSelected,
showCallOutUnauthorizedMsg,
showTimeline,
startTimelineSaving,
- toggleDetailPanel,
unPinEvent,
updateAutoSaveMsg,
- updateColumns,
updateDataProviderEnabled,
updateDataProviderExcluded,
updateDataProviderKqlQuery,
@@ -47,18 +37,13 @@ import {
updateIndexNames,
updateIsFavorite,
updateIsLive,
- updateIsLoading,
- updateItemsPerPage,
- updateItemsPerPageOptions,
updateKqlMode,
updatePageIndex,
updateProviders,
updateRange,
- updateSort,
updateTimeline,
updateTimelineGraphEventId,
updateTitleAndDescription,
- upsertColumn,
toggleModalSaveTimeline,
updateEqlOptions,
} from './actions';
@@ -69,23 +54,15 @@ import {
addTimelineNoteToEvent,
addTimelineProvider,
addTimelineToStore,
- applyDeltaToTimelineColumnWidth,
applyKqlFilterQueryDraft,
pinTimelineEvent,
- removeTimelineColumn,
removeTimelineProvider,
- setDeletedTimelineEvents,
- setLoadingTimelineEvents,
- setSelectedTimelineEvents,
unPinTimelineEvent,
updateExcludedRowRenderersIds,
- updateTimelineColumns,
updateTimelineIsFavorite,
updateTimelineIsLive,
- updateTimelineItemsPerPage,
updateTimelineKqlMode,
updateTimelinePageIndex,
- updateTimelinePerPageOptions,
updateTimelineProviderEnabled,
updateTimelineProviderExcluded,
updateTimelineProviderProperties,
@@ -94,13 +71,10 @@ import {
updateTimelineProviders,
updateTimelineRange,
updateTimelineShowTimeline,
- updateTimelineSort,
updateTimelineTitleAndDescription,
- upsertTimelineColumn,
updateSavedQuery,
updateGraphEventId,
updateFilters,
- updateTimelineDetailsPanel,
updateTimelineEventType,
} from './helpers';
@@ -123,53 +97,17 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: addTimelineToStore({ id, timeline, timelineById: state.timelineById }),
}))
- .case(
- createTimeline,
- (
- state,
- {
+ .case(createTimeline, (state, { id, timelineType = TimelineType.default, ...timelineProps }) => {
+ return {
+ ...state,
+ timelineById: addNewTimeline({
id,
- dataProviders,
- dateRange,
- excludedRowRendererIds,
- expandedDetail = {},
- show,
- columns,
- itemsPerPage,
- indexNames,
- kqlQuery,
- sort,
- showCheckboxes,
- timelineType = TimelineType.default,
- filters,
- }
- ) => {
- return {
- ...state,
- timelineById: addNewTimeline({
- columns,
- dataProviders,
- dateRange,
- excludedRowRendererIds,
- expandedDetail,
- filters,
- id,
- itemsPerPage,
- indexNames,
- kqlQuery,
- sort,
- show,
- showCheckboxes,
- timelineById: state.timelineById,
- timelineType,
- }),
- };
- }
- )
- .case(upsertColumn, (state, { column, id, index }) => ({
- ...state,
- timelineById: upsertTimelineColumn({ column, id, index, timelineById: state.timelineById }),
- }))
+ timelineById: state.timelineById,
+ timelineType,
+ ...timelineProps,
+ }),
+ };
+ })
.case(addHistory, (state, { id, historyId }) => ({
...state,
timelineById: addTimelineHistory({ id, historyId, timelineById: state.timelineById }),
@@ -182,19 +120,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }),
}))
- .case(toggleDetailPanel, (state, action) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [action.timelineId]: {
- ...state.timelineById[action.timelineId],
- expandedDetail: {
- ...state.timelineById[action.timelineId].expandedDetail,
- ...updateTimelineDetailsPanel(action),
- },
- },
- },
- }))
.case(addProvider, (state, { id, provider }) => ({
...state,
timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }),
@@ -215,27 +140,10 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateGraphEventId({ id, graphEventId, timelineById: state.timelineById }),
}))
- .case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({
- ...state,
- timelineById: applyDeltaToTimelineColumnWidth({
- id,
- columnId,
- delta,
- timelineById: state.timelineById,
- }),
- }))
.case(pinEvent, (state, { id, eventId }) => ({
...state,
timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }),
}))
- .case(removeColumn, (state, { id, columnId }) => ({
- ...state,
- timelineById: removeTimelineColumn({
- id,
- columnId,
- timelineById: state.timelineById,
- }),
- }))
.case(removeProvider, (state, { id, providerId, andProviderId }) => ({
...state,
timelineById: removeTimelineProvider({
@@ -265,44 +173,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
},
},
}))
- .case(setEventsDeleted, (state, { id, eventIds, isDeleted }) => ({
- ...state,
- timelineById: setDeletedTimelineEvents({
- id,
- eventIds,
- timelineById: state.timelineById,
- isDeleted,
- }),
- }))
- .case(clearEventsDeleted, (state, { id }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- deletedEventIds: [],
- },
- },
- }))
- .case(setEventsLoading, (state, { id, eventIds, isLoading }) => ({
- ...state,
- timelineById: setLoadingTimelineEvents({
- id,
- eventIds,
- timelineById: state.timelineById,
- isLoading,
- }),
- }))
- .case(clearEventsLoading, (state, { id }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- loadingEventIds: [],
- },
- },
- }))
.case(setExcludedRowRendererIds, (state, { id, excludedRowRendererIds }) => ({
...state,
timelineById: updateExcludedRowRenderersIds({
@@ -311,37 +181,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
- .case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({
- ...state,
- timelineById: setSelectedTimelineEvents({
- id,
- eventIds,
- timelineById: state.timelineById,
- isSelected,
- isSelectAllChecked,
- }),
- }))
- .case(clearSelected, (state, { id }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- selectedEventIds: {},
- isSelectAllChecked: false,
- },
- },
- }))
- .case(updateIsLoading, (state, { id, isLoading }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- isLoading,
- },
- },
- }))
.case(updateTimeline, (state, { id, timeline }) => ({
...state,
timelineById: {
@@ -353,14 +192,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: unPinTimelineEvent({ id, eventId, timelineById: state.timelineById }),
}))
- .case(updateColumns, (state, { id, columns }) => ({
- ...state,
- timelineById: updateTimelineColumns({
- id,
- columns,
- timelineById: state.timelineById,
- }),
- }))
.case(updateEventType, (state, { id, eventType }) => ({
...state,
timelineById: updateTimelineEventType({ id, eventType, timelineById: state.timelineById }),
@@ -394,10 +225,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateTimelineRange({ id, start, end, timelineById: state.timelineById }),
}))
- .case(updateSort, (state, { id, sort }) => ({
- ...state,
- timelineById: updateTimelineSort({ id, sort, timelineById: state.timelineById }),
- }))
.case(updateDataProviderEnabled, (state, { id, enabled, providerId, andProviderId }) => ({
...state,
timelineById: updateTimelineProviderEnabled({
@@ -454,14 +281,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
- .case(updateItemsPerPage, (state, { id, itemsPerPage }) => ({
- ...state,
- timelineById: updateTimelineItemsPerPage({
- id,
- itemsPerPage,
- timelineById: state.timelineById,
- }),
- }))
.case(updatePageIndex, (state, { id, activePage }) => ({
...state,
timelineById: updateTimelinePageIndex({
@@ -470,14 +289,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
- .case(updateItemsPerPageOptions, (state, { id, itemsPerPageOptions }) => ({
- ...state,
- timelineById: updateTimelinePerPageOptions({
- id,
- itemsPerPageOptions,
- timelineById: state.timelineById,
- }),
- }))
.case(updateAutoSaveMsg, (state, { timelineId, newTimelineModel }) => ({
...state,
autoSavedWarningMsg: {
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts
index b05e6568be6c3..f46b55bcd3345 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts
@@ -7,11 +7,14 @@
import { createSelector } from 'reselect';
+import { tGridSelectors } from '../../../../../timelines/public';
import { State } from '../../../common/store/types';
import { TimelineModel } from './model';
import { AutoSavedWarningMsg, InsertTimeline, TimelineById } from './types';
+export const { getManageTimelineById } = tGridSelectors;
+
const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById;
const selectAutoSaveMsg = (state: State): AutoSavedWarningMsg => state.timeline.autoSavedWarningMsg;
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index d4e2601554187..aad685f9fb103 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -23,6 +23,7 @@ import {
} from '../../triggers_actions_ui/public';
import { CasesUiStart } from '../../cases/public';
import { SecurityPluginSetup } from '../../security/public';
+import { TimelinesUIStart } from '../../timelines/public';
import { ResolverPluginSetup } from './resolver/types';
import { Inspect } from '../common/search_strategy';
import { MlPluginSetup, MlPluginStart } from '../../ml/public';
@@ -56,6 +57,7 @@ export interface StartPlugins {
licensing: LicensingPluginStart;
newsfeed?: NewsfeedPublicPluginStart;
triggersActionsUi: TriggersActionsStart;
+ timelines: TimelinesUIStart;
uiActions: UiActionsStart;
ml?: MlPluginStart;
}
diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts
index 72f56f13eaddf..66ac744b3a50c 100644
--- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts
+++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts
@@ -15,9 +15,16 @@ import { KbnClient } from '@kbn/test';
import { AxiosResponse } from 'axios';
import { indexHostsAndAlerts } from '../../common/endpoint/index_data';
import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data';
-import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../../fleet/common/constants';
import {
+ AGENTS_SETUP_API_ROUTES,
+ EPM_API_ROUTES,
+ SETUP_API_ROUTE,
+} from '../../../fleet/common/constants';
+import {
+ BulkInstallPackageInfo,
+ BulkInstallPackagesResponse,
CreateFleetSetupResponse,
+ IBulkInstallPackageHTTPError,
PostIngestSetupResponse,
} from '../../../fleet/common/types/rest_spec';
import { KbnClientWithApiKeySupport } from './kbn_client_with_api_key_support';
@@ -44,6 +51,12 @@ async function deleteIndices(indices: string[], client: Client) {
}
}
+function isFleetBulkInstallError(
+ installResponse: BulkInstallPackageInfo | IBulkInstallPackageHTTPError
+): installResponse is IBulkInstallPackageHTTPError {
+ return 'error' in installResponse && installResponse.error !== undefined;
+}
+
async function doIngestSetup(kbnClient: KbnClient) {
// Setup Ingest
try {
@@ -76,6 +89,35 @@ async function doIngestSetup(kbnClient: KbnClient) {
console.error(error);
throw error;
}
+
+ // Install/upgrade the endpoint package
+ try {
+ const installEndpointPackageResp = (await kbnClient.request({
+ path: EPM_API_ROUTES.BULK_INSTALL_PATTERN,
+ method: 'POST',
+ body: {
+ packages: ['endpoint'],
+ },
+ })) as AxiosResponse;
+
+ const bulkResp = installEndpointPackageResp.data.response;
+ if (bulkResp.length <= 0) {
+ throw new Error('Installing the Endpoint package failed, response was empty, existing');
+ }
+
+ if (isFleetBulkInstallError(bulkResp[0])) {
+ if (bulkResp[0].error instanceof Error) {
+ throw new Error(
+ `Installing the Endpoint package failed: ${bulkResp[0].error.message}, exiting`
+ );
+ }
+
+ throw new Error(bulkResp[0].error);
+ }
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
}
async function main() {
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts
index 20b29694a1df1..1a8b17bf19e18 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts
@@ -56,7 +56,6 @@ export const getAuditLogResponse = async ({
context: SecuritySolutionRequestHandlerContext;
logger: Logger;
}): Promise<{
- total: number;
page: number;
pageSize: number;
data: Array<{
@@ -96,10 +95,6 @@ export const getAuditLogResponse = async ({
}
return {
- total:
- typeof result.body.hits.total === 'number'
- ? result.body.hits.total
- : result.body.hits.total.value,
page,
pageSize,
data: result.body.hits.hits.map((e) => ({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts
new file mode 100644
index 0000000000000..76389d7376fc8
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts
@@ -0,0 +1,791 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EqlSearchStrategyResponse } from '../../../../../../../../src/plugins/data/common';
+import { EqlSearchResponse } from '../../../../../common/detection_engine/types';
+
+export const sequenceResponse = ({
+ rawResponse: {
+ body: {
+ is_partial: false,
+ is_running: false,
+ took: 527,
+ timed_out: false,
+ hits: {
+ total: {
+ value: 10,
+ relation: 'eq',
+ },
+ sequences: [
+ {
+ join_keys: ['win2019-endpoint-mr-pedro'],
+ events: [
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'qhymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ name: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3377092Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293866,
+ ingested: '2021-02-08T21:57:26.417559711Z',
+ created: '2021-02-08T21:50:28.3377092Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'log_on',
+ id: 'LzzWB9jjGmCwGMvk++++FG/O',
+ category: ['authentication', 'session'],
+ type: ['start'],
+ dataset: 'endpoint.events.security',
+ outcome: 'success',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'qxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3377142Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293867,
+ ingested: '2021-02-08T21:57:26.417596906Z',
+ created: '2021-02-08T21:50:28.3377142Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'log_on',
+ id: 'LzzWB9jjGmCwGMvk++++FG/P',
+ category: ['authentication', 'session'],
+ type: ['start'],
+ dataset: 'endpoint.events.security',
+ outcome: 'success',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'rBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3381013Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293868,
+ ingested: '2021-02-08T21:57:26.417632166Z',
+ created: '2021-02-08T21:50:28.3381013Z',
+ kind: 'event',
+ module: 'endpoint',
+ id: 'LzzWB9jjGmCwGMvk++++FG/Q',
+ category: [],
+ type: [],
+ dataset: 'endpoint.events.security',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ ],
+ },
+ {
+ join_keys: ['win2019-endpoint-mr-pedro'],
+ events: [
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'qxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3377142Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293867,
+ ingested: '2021-02-08T21:57:26.417596906Z',
+ created: '2021-02-08T21:50:28.3377142Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'log_on',
+ id: 'LzzWB9jjGmCwGMvk++++FG/P',
+ category: ['authentication', 'session'],
+ type: ['start'],
+ dataset: 'endpoint.events.security',
+ outcome: 'success',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'rBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3381013Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293868,
+ ingested: '2021-02-08T21:57:26.417632166Z',
+ created: '2021-02-08T21:50:28.3381013Z',
+ kind: 'event',
+ module: 'endpoint',
+ id: 'LzzWB9jjGmCwGMvk++++FG/Q',
+ category: [],
+ type: [],
+ dataset: 'endpoint.events.security',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.process-default-2021.02.02-000005',
+ _id: 'pxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ code_signature: [
+ {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ ],
+ token: {
+ integrity_level_name: 'high',
+ elevation_level: 'default',
+ },
+ },
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-y'],
+ parent: {
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-R'],
+ name: 'sshd.exe',
+ pid: 5284,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -R',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ },
+ code_signature: {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ name: 'sshd.exe',
+ pid: 6368,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTYzNjgtMTMyNTcyOTQ2MjguMzQ0NjM1NTAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -y',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ hash: {
+ sha1: '631244d731f406394c17c7dfd85203e317c74814',
+ sha256: 'e6a972f9db27de18be225095b3b3141b945be8aadc4014c8704ae5acafe3e8e0',
+ md5: '331ba0e529810ef718dd3efbd1242302',
+ },
+ },
+ message: 'Endpoint process event',
+ '@timestamp': '2021-02-08T21:50:28.3446355Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.process',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293863,
+ ingested: '2021-02-08T21:57:26.417387865Z',
+ created: '2021-02-08T21:50:28.3446355Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'start',
+ id: 'LzzWB9jjGmCwGMvk++++FG/K',
+ category: ['process'],
+ type: ['start'],
+ dataset: 'endpoint.events.process',
+ },
+ user: {
+ domain: '',
+ name: '',
+ },
+ },
+ },
+ ],
+ },
+ {
+ join_keys: ['win2019-endpoint-mr-pedro'],
+ events: [
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'rBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3381013Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293868,
+ ingested: '2021-02-08T21:57:26.417632166Z',
+ created: '2021-02-08T21:50:28.3381013Z',
+ kind: 'event',
+ module: 'endpoint',
+ id: 'LzzWB9jjGmCwGMvk++++FG/Q',
+ category: [],
+ type: [],
+ dataset: 'endpoint.events.security',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.process-default-2021.02.02-000005',
+ _id: 'pxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ code_signature: [
+ {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ ],
+ token: {
+ integrity_level_name: 'high',
+ elevation_level: 'default',
+ },
+ },
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-y'],
+ parent: {
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-R'],
+ name: 'sshd.exe',
+ pid: 5284,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -R',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ },
+ code_signature: {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ name: 'sshd.exe',
+ pid: 6368,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTYzNjgtMTMyNTcyOTQ2MjguMzQ0NjM1NTAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -y',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ hash: {
+ sha1: '631244d731f406394c17c7dfd85203e317c74814',
+ sha256: 'e6a972f9db27de18be225095b3b3141b945be8aadc4014c8704ae5acafe3e8e0',
+ md5: '331ba0e529810ef718dd3efbd1242302',
+ },
+ },
+ message: 'Endpoint process event',
+ '@timestamp': '2021-02-08T21:50:28.3446355Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.process',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293863,
+ ingested: '2021-02-08T21:57:26.417387865Z',
+ created: '2021-02-08T21:50:28.3446355Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'start',
+ id: 'LzzWB9jjGmCwGMvk++++FG/K',
+ category: ['process'],
+ type: ['start'],
+ dataset: 'endpoint.events.process',
+ },
+ user: {
+ domain: '',
+ name: '',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.network-default-2021.02.02-000005',
+ _id: 'qBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ name: 'svchost.exe',
+ pid: 968,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTk2OC0xMzI1NTA3ODY3My4yNjQyNDcyMDA=',
+ executable: 'C:\\Windows\\System32\\svchost.exe',
+ },
+ destination: {
+ address: '10.128.0.57',
+ port: 3389,
+ bytes: 1681,
+ ip: '10.128.0.57',
+ },
+ source: {
+ address: '142.202.189.139',
+ port: 16151,
+ bytes: 1224,
+ ip: '142.202.189.139',
+ },
+ message: 'Endpoint network event',
+ network: {
+ transport: 'tcp',
+ type: 'ipv4',
+ direction: 'incoming',
+ },
+ '@timestamp': '2021-02-08T21:50:28.5553532Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.network',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293864,
+ ingested: '2021-02-08T21:57:26.417451347Z',
+ created: '2021-02-08T21:50:28.5553532Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'disconnect_received',
+ id: 'LzzWB9jjGmCwGMvk++++FG/L',
+ category: ['network'],
+ type: ['end'],
+ dataset: 'endpoint.events.network',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'NETWORK SERVICE',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ statusCode: 200,
+ headers: {},
+ meta: {},
+ hits: {},
+ },
+} as unknown) as EqlSearchStrategyResponse>;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts
index 6529c594dd5a5..da5c89a3102a1 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts
@@ -7,10 +7,8 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
-
-import { sequenceResponse } from '../../../search_strategy/timeline/eql/__mocks__';
-
import { createEqlAlertType } from './eql';
+import { sequenceResponse } from './__mocks__/eql';
import { createRuleTypeMocks } from './__mocks__/rule_type';
describe('EQL alerts', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts
index 94e70e4eb001b..3a37a49d03dcd 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts
@@ -7,13 +7,20 @@
import { AuthenticatedUser } from '../../../../../../security/common/model';
-import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
+import { TimelineStatus, TimelineType, SavedTimeline } from '../../../../../common/types/timeline';
+import { NoteSavedObject } from '../../../../../common/types/timeline/note';
import { pickSavedTimeline } from './pick_saved_timeline';
describe('pickSavedTimeline', () => {
const mockDateNow = new Date('2020-04-03T23:00:00.000Z').valueOf();
- const getMockSavedTimeline = () => ({
+ const getMockSavedTimeline = (): SavedTimeline & {
+ savedObjectId?: string | null;
+ version?: string;
+ eventNotes?: NoteSavedObject[];
+ globalNotes?: NoteSavedObject[];
+ pinnedEventIds?: [];
+ } => ({
savedObjectId: '7af80430-03f4-11eb-9d9d-ffba20fabba8',
version: 'WzQ0ODgsMV0=',
created: 1601563413330,
@@ -91,7 +98,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -113,7 +120,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -143,7 +150,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline with a new title', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -152,7 +159,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline without title', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -161,7 +168,7 @@ describe('pickSavedTimeline', () => {
test('Updating an immutable timeline with a new title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.immutable };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -192,7 +199,7 @@ describe('pickSavedTimeline', () => {
test('Updating an untitled draft timeline with a title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -201,7 +208,7 @@ describe('pickSavedTimeline', () => {
test('Updating a draft timeline with a new title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -210,7 +217,7 @@ describe('pickSavedTimeline', () => {
test('Updating a draft timeline without title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts
index a28084cd78154..3e00a33966f17 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts
@@ -12,10 +12,9 @@ import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../../comm
export const pickSavedTimeline = (
timelineId: string | null,
- savedTimeline: SavedTimeline,
+ savedTimeline: SavedTimeline & { savedObjectId?: string | null },
userInfo: AuthenticatedUser | null
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
-): any => {
+): SavedTimeline & { savedObjectId?: string | null } => {
const dateNow = new Date().valueOf();
if (timelineId == null) {
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index ac9d854f18211..4bcbcb71d048c 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -83,8 +83,6 @@ import { initUsageCollectors } from './usage';
import type { SecuritySolutionRequestHandlerContext } from './types';
import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps';
import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution';
-import { securitySolutionIndexFieldsProvider } from './search_strategy/index_fields';
-import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline';
import { TelemetryEventsSender } from './lib/telemetry/sender';
import {
TelemetryPluginStart,
@@ -92,7 +90,6 @@ import {
} from '../../../../src/plugins/telemetry/server';
import { licenseService } from './lib/license';
import { PolicyWatcher } from './endpoint/lib/policy/license_watch';
-import { securitySolutionTimelineEqlSearchStrategyProvider } from './search_strategy/timeline/eql';
import { parseExperimentalConfigValue } from '../common/experimental_features';
import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet';
@@ -451,30 +448,10 @@ export class Plugin implements IPlugin {
- describe('#formatTimelineData', () => {
- it('happy path', async () => {
- const res = await formatTimelineData(
- [
- '@timestamp',
- 'host.name',
- 'destination.ip',
- 'source.ip',
- 'source.geo.location',
- 'threat.indicator.matched.field',
- ],
- TIMELINE_EVENTS_FIELDS,
- eventHit
- );
- expect(res).toEqual({
- cursor: {
- tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239',
- value: '1605624488922',
- },
- node: {
- _id: 'tkCt1nUBaEgqnrVSZ8R_',
- _index: 'auditbeat-7.8.0-2020.11.05-000003',
- data: [
- {
- field: '@timestamp',
- value: ['2020-11-17T14:48:08.922Z'],
- },
- {
- field: 'host.name',
- value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- },
- {
- field: 'threat.indicator.matched.field',
- value: ['matched_field', 'other_matched_field', 'matched_field_2'],
- },
- {
- field: 'source.geo.location',
- value: [`{"lon":118.7778,"lat":32.0617}`],
- },
- ],
- ecs: {
- '@timestamp': ['2020-11-17T14:48:08.922Z'],
- _id: 'tkCt1nUBaEgqnrVSZ8R_',
- _index: 'auditbeat-7.8.0-2020.11.05-000003',
- agent: {
- type: ['auditbeat'],
- },
- event: {
- action: ['process_started'],
- category: ['process'],
- dataset: ['process'],
- kind: ['event'],
- module: ['system'],
- type: ['start'],
- },
- host: {
- id: ['e59991e835905c65ed3e455b33e13bd6'],
- ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
- name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- os: {
- family: ['debian'],
- },
- },
- message: ['Process go (PID: 4313) by user jenkins STARTED'],
- process: {
- args: ['go', 'vet', './...'],
- entity_id: ['Z59cIkAAIw8ZoK0H'],
- executable: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
- ],
- hash: {
- sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
- },
- name: ['go'],
- pid: ['4313'],
- ppid: ['3977'],
- working_directory: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
- ],
- },
- timestamp: '2020-11-17T14:48:08.922Z',
- user: {
- name: ['jenkins'],
- },
- threat: {
- indicator: [
- {
- event: {
- dataset: [],
- reference: [],
- },
- matched: {
- atomic: ['matched_atomic'],
- field: ['matched_field', 'other_matched_field'],
- type: [],
- },
- provider: ['yourself'],
- },
- {
- event: {
- dataset: [],
- reference: [],
- },
- matched: {
- atomic: ['matched_atomic_2'],
- field: ['matched_field_2'],
- type: [],
- },
- provider: ['other_you'],
- },
- ],
- },
- },
- },
- });
- });
-
- it('rule signal results', async () => {
- const response: EventHit = {
- _index: '.siem-signals-patrykkopycinski-default-000007',
- _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562',
- _score: 0,
- _source: {
- signal: {
- threshold_result: {
- count: 10000,
- value: '2a990c11-f61b-4c8e-b210-da2574e9f9db',
- },
- parent: {
- depth: 0,
- index:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- id: '0268af90-d8da-576a-9747-2a191519416a',
- type: 'event',
- },
- depth: 1,
- _meta: {
- version: 14,
- },
- rule: {
- note: null,
- throttle: null,
- references: [],
- severity_mapping: [],
- description: 'asdasd',
- created_at: '2021-01-09T11:25:45.046Z',
- language: 'kuery',
- threshold: {
- field: '',
- value: 200,
- },
- building_block_type: null,
- output_index: '.siem-signals-patrykkopycinski-default',
- type: 'threshold',
- rule_name_override: null,
- enabled: true,
- exceptions_list: [],
- updated_at: '2021-01-09T13:36:39.204Z',
- timestamp_override: null,
- from: 'now-360s',
- id: '696c24e0-526d-11eb-836c-e1620268b945',
- timeline_id: null,
- max_signals: 100,
- severity: 'low',
- risk_score: 21,
- risk_score_mapping: [],
- author: [],
- query: '_id :*',
- index: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- filters: [
- {
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: null,
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_index',
- },
- exists: {
- field: '_index',
- },
- },
- {
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: 'id_exists',
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_id',
- },
- exists: {
- field: '_id',
- },
- },
- ],
- created_by: 'patryk_test_user',
- version: 1,
- saved_id: null,
- tags: [],
- rule_id: '2a990c11-f61b-4c8e-b210-da2574e9f9db',
- license: '',
- immutable: false,
- timeline_title: null,
- meta: {
- from: '1m',
- kibana_siem_app_url: 'http://localhost:5601/app/security',
- },
- name: 'Threshold test',
- updated_by: 'patryk_test_user',
- interval: '5m',
- false_positives: [],
- to: 'now',
- threat: [],
- actions: [],
- },
- original_time: '2021-01-09T13:39:32.595Z',
- ancestors: [
- {
- depth: 0,
- index:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- id: '0268af90-d8da-576a-9747-2a191519416a',
- type: 'event',
- },
- ],
- parents: [
- {
- depth: 0,
- index:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- id: '0268af90-d8da-576a-9747-2a191519416a',
- type: 'event',
- },
- ],
- status: 'open',
- },
- },
- fields: {
- 'signal.rule.output_index': ['.siem-signals-patrykkopycinski-default'],
- 'signal.rule.from': ['now-360s'],
- 'signal.rule.language': ['kuery'],
- '@timestamp': ['2021-01-09T13:41:40.517Z'],
- 'signal.rule.query': ['_id :*'],
- 'signal.rule.type': ['threshold'],
- 'signal.rule.id': ['696c24e0-526d-11eb-836c-e1620268b945'],
- 'signal.rule.risk_score': [21],
- 'signal.status': ['open'],
- 'event.kind': ['signal'],
- 'signal.original_time': ['2021-01-09T13:39:32.595Z'],
- 'signal.rule.severity': ['low'],
- 'signal.rule.version': ['1'],
- 'signal.rule.index': [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- 'signal.rule.name': ['Threshold test'],
- 'signal.rule.to': ['now'],
- },
- _type: '',
- sort: ['1610199700517'],
- aggregations: {},
- };
-
- expect(
- await formatTimelineData(
- ['@timestamp', 'host.name', 'destination.ip', 'source.ip'],
- TIMELINE_EVENTS_FIELDS,
- response
- )
- ).toEqual({
- cursor: {
- tiebreaker: null,
- value: '',
- },
- node: {
- _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562',
- _index: '.siem-signals-patrykkopycinski-default-000007',
- data: [
- {
- field: '@timestamp',
- value: ['2021-01-09T13:41:40.517Z'],
- },
- ],
- ecs: {
- '@timestamp': ['2021-01-09T13:41:40.517Z'],
- timestamp: '2021-01-09T13:41:40.517Z',
- _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562',
- _index: '.siem-signals-patrykkopycinski-default-000007',
- event: {
- kind: ['signal'],
- },
- signal: {
- original_time: ['2021-01-09T13:39:32.595Z'],
- status: ['open'],
- threshold_result: ['{"count":10000,"value":"2a990c11-f61b-4c8e-b210-da2574e9f9db"}'],
- rule: {
- building_block_type: [],
- exceptions_list: [],
- from: ['now-360s'],
- id: ['696c24e0-526d-11eb-836c-e1620268b945'],
- index: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- language: ['kuery'],
- name: ['Threshold test'],
- output_index: ['.siem-signals-patrykkopycinski-default'],
- risk_score: ['21'],
- query: ['_id :*'],
- severity: ['low'],
- to: ['now'],
- type: ['threshold'],
- version: ['1'],
- timeline_id: [],
- timeline_title: [],
- saved_id: [],
- note: [],
- threshold: [
- JSON.stringify({
- field: '',
- value: 200,
- }),
- ],
- filters: [
- JSON.stringify({
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: null,
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_index',
- },
- exists: {
- field: '_index',
- },
- }),
- JSON.stringify({
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: 'id_exists',
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_id',
- },
- exists: {
- field: '_id',
- },
- }),
- ],
- },
- },
- },
- },
- });
- });
- });
-
- describe('#buildObjectForFieldPath', () => {
- it('builds an object from a single non-nested field', () => {
- expect(buildObjectForFieldPath('@timestamp', eventHit)).toEqual({
- '@timestamp': ['2020-11-17T14:48:08.922Z'],
- });
- });
-
- it('builds an object with no fields response', () => {
- const { fields, ...fieldLessHit } = eventHit;
- // @ts-expect-error fieldLessHit is intentionally missing fields
- expect(buildObjectForFieldPath('@timestamp', fieldLessHit)).toEqual({
- '@timestamp': [],
- });
- });
-
- it('does not misinterpret non-nested fields with a common prefix', () => {
- // @ts-expect-error hit is minimal
- const hit: EventHit = {
- fields: {
- 'foo.bar': ['baz'],
- 'foo.barBaz': ['foo'],
- },
- };
-
- expect(buildObjectForFieldPath('foo.barBaz', hit)).toEqual({
- foo: { barBaz: ['foo'] },
- });
- });
-
- it('builds an array of objects from a nested field', () => {
- // @ts-expect-error hit is minimal
- const hit: EventHit = {
- fields: {
- foo: [{ bar: ['baz'] }],
- },
- };
- expect(buildObjectForFieldPath('foo.bar', hit)).toEqual({
- foo: [{ bar: ['baz'] }],
- });
- });
-
- it('builds intermediate objects for nested fields', () => {
- // @ts-expect-error nestedHit is minimal
- const nestedHit: EventHit = {
- fields: {
- 'foo.bar': [
- {
- baz: ['host.name'],
- },
- ],
- },
- };
- expect(buildObjectForFieldPath('foo.bar.baz', nestedHit)).toEqual({
- foo: {
- bar: [
- {
- baz: ['host.name'],
- },
- ],
- },
- });
- });
-
- it('builds intermediate objects at multiple levels', () => {
- expect(buildObjectForFieldPath('threat.indicator.matched.atomic', eventHit)).toEqual({
- threat: {
- indicator: [
- {
- matched: {
- atomic: ['matched_atomic'],
- },
- },
- {
- matched: {
- atomic: ['matched_atomic_2'],
- },
- },
- ],
- },
- });
- });
-
- it('preserves multiple values for a single leaf', () => {
- expect(buildObjectForFieldPath('threat.indicator.matched.field', eventHit)).toEqual({
- threat: {
- indicator: [
- {
- matched: {
- field: ['matched_field', 'other_matched_field'],
- },
- },
- {
- matched: {
- field: ['matched_field_2'],
- },
- },
- ],
- },
- });
- });
-
- describe('multiple levels of nested fields', () => {
- let nestedHit: EventHit;
-
- beforeEach(() => {
- // @ts-expect-error nestedHit is minimal
- nestedHit = {
- fields: {
- 'nested_1.foo': [
- {
- 'nested_2.bar': [
- { leaf: ['leaf_value'], leaf_2: ['leaf_2_value'] },
- { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] },
- ],
- },
- {
- 'nested_2.bar': [
- { leaf: ['leaf_value_2'], leaf_2: ['leaf_2_value_4'] },
- { leaf: ['leaf_value_3'], leaf_2: ['leaf_2_value_5'] },
- ],
- },
- ],
- },
- };
- });
-
- it('includes objects without the field', () => {
- expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf', nestedHit)).toEqual({
- nested_1: {
- foo: [
- {
- nested_2: {
- bar: [{ leaf: ['leaf_value'] }, { leaf: [] }],
- },
- },
- {
- nested_2: {
- bar: [{ leaf: ['leaf_value_2'] }, { leaf: ['leaf_value_3'] }],
- },
- },
- ],
- },
- });
- });
-
- it('groups multiple leaf values', () => {
- expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf_2', nestedHit)).toEqual({
- nested_1: {
- foo: [
- {
- nested_2: {
- bar: [
- { leaf_2: ['leaf_2_value'] },
- { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] },
- ],
- },
- },
- {
- nested_2: {
- bar: [{ leaf_2: ['leaf_2_value_4'] }, { leaf_2: ['leaf_2_value_5'] }],
- },
- },
- ],
- },
- });
- });
- });
- });
-
- describe('#buildFieldsRequest', () => {
- it('happy path', async () => {
- const res = await buildFieldsRequest([
- '@timestamp',
- 'host.name',
- 'destination.ip',
- 'source.ip',
- 'source.geo.location',
- 'threat.indicator.matched.field',
- ]);
- expect(res).toEqual([
- {
- field: '@timestamp',
- include_unmapped: true,
- },
- {
- field: 'host.name',
- include_unmapped: true,
- },
- {
- field: 'destination.ip',
- include_unmapped: true,
- },
- {
- field: 'source.ip',
- include_unmapped: true,
- },
- {
- field: 'source.geo.location',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.matched.field',
- include_unmapped: true,
- },
- {
- field: 'signal.status',
- include_unmapped: true,
- },
- {
- field: 'signal.group.id',
- include_unmapped: true,
- },
- {
- field: 'signal.original_time',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.filters',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.from',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.language',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.query',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.name',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.to',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.id',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.index',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.type',
- include_unmapped: true,
- },
- {
- field: 'signal.original_event.kind',
- include_unmapped: true,
- },
- {
- field: 'signal.original_event.module',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.version',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.severity',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.risk_score',
- include_unmapped: true,
- },
- {
- field: 'signal.threshold_result',
- include_unmapped: true,
- },
- {
- field: 'event.code',
- include_unmapped: true,
- },
- {
- field: 'event.module',
- include_unmapped: true,
- },
- {
- field: 'event.action',
- include_unmapped: true,
- },
- {
- field: 'event.category',
- include_unmapped: true,
- },
- {
- field: 'user.name',
- include_unmapped: true,
- },
- {
- field: 'message',
- include_unmapped: true,
- },
- {
- field: 'system.auth.ssh.signature',
- include_unmapped: true,
- },
- {
- field: 'system.auth.ssh.method',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.arch',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.entity_id',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.name',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.size',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.summary',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.version',
- include_unmapped: true,
- },
- {
- field: 'event.created',
- include_unmapped: true,
- },
- {
- field: 'event.dataset',
- include_unmapped: true,
- },
- {
- field: 'event.duration',
- include_unmapped: true,
- },
- {
- field: 'event.end',
- include_unmapped: true,
- },
- {
- field: 'event.hash',
- include_unmapped: true,
- },
- {
- field: 'event.id',
- include_unmapped: true,
- },
- {
- field: 'event.kind',
- include_unmapped: true,
- },
- {
- field: 'event.original',
- include_unmapped: true,
- },
- {
- field: 'event.outcome',
- include_unmapped: true,
- },
- {
- field: 'event.risk_score',
- include_unmapped: true,
- },
- {
- field: 'event.risk_score_norm',
- include_unmapped: true,
- },
- {
- field: 'event.severity',
- include_unmapped: true,
- },
- {
- field: 'event.start',
- include_unmapped: true,
- },
- {
- field: 'event.timezone',
- include_unmapped: true,
- },
- {
- field: 'event.type',
- include_unmapped: true,
- },
- {
- field: 'agent.type',
- include_unmapped: true,
- },
- {
- field: 'auditd.result',
- include_unmapped: true,
- },
- {
- field: 'auditd.session',
- include_unmapped: true,
- },
- {
- field: 'auditd.data.acct',
- include_unmapped: true,
- },
- {
- field: 'auditd.data.terminal',
- include_unmapped: true,
- },
- {
- field: 'auditd.data.op',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.actor.primary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.actor.secondary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.object.primary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.object.secondary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.object.type',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.how',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.message_type',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.sequence',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.original.path',
- include_unmapped: true,
- },
- {
- field: 'file.name',
- include_unmapped: true,
- },
- {
- field: 'file.target_path',
- include_unmapped: true,
- },
- {
- field: 'file.extension',
- include_unmapped: true,
- },
- {
- field: 'file.type',
- include_unmapped: true,
- },
- {
- field: 'file.device',
- include_unmapped: true,
- },
- {
- field: 'file.inode',
- include_unmapped: true,
- },
- {
- field: 'file.uid',
- include_unmapped: true,
- },
- {
- field: 'file.owner',
- include_unmapped: true,
- },
- {
- field: 'file.gid',
- include_unmapped: true,
- },
- {
- field: 'file.group',
- include_unmapped: true,
- },
- {
- field: 'file.mode',
- include_unmapped: true,
- },
- {
- field: 'file.size',
- include_unmapped: true,
- },
- {
- field: 'file.mtime',
- include_unmapped: true,
- },
- {
- field: 'file.ctime',
- include_unmapped: true,
- },
- {
- field: 'file.path',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.code_signature',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.code_signature.subject_name',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.code_signature.trusted',
- include_unmapped: true,
- },
- {
- field: 'file.hash.sha256',
- include_unmapped: true,
- },
- {
- field: 'host.os.family',
- include_unmapped: true,
- },
- {
- field: 'host.id',
- include_unmapped: true,
- },
- {
- field: 'host.ip',
- include_unmapped: true,
- },
- {
- field: 'registry.key',
- include_unmapped: true,
- },
- {
- field: 'registry.path',
- include_unmapped: true,
- },
- {
- field: 'rule.reference',
- include_unmapped: true,
- },
- {
- field: 'source.bytes',
- include_unmapped: true,
- },
- {
- field: 'source.packets',
- include_unmapped: true,
- },
- {
- field: 'source.port',
- include_unmapped: true,
- },
- {
- field: 'source.geo.continent_name',
- include_unmapped: true,
- },
- {
- field: 'source.geo.country_name',
- include_unmapped: true,
- },
- {
- field: 'source.geo.country_iso_code',
- include_unmapped: true,
- },
- {
- field: 'source.geo.city_name',
- include_unmapped: true,
- },
- {
- field: 'source.geo.region_iso_code',
- include_unmapped: true,
- },
- {
- field: 'source.geo.region_name',
- include_unmapped: true,
- },
- {
- field: 'destination.bytes',
- include_unmapped: true,
- },
- {
- field: 'destination.packets',
- include_unmapped: true,
- },
- {
- field: 'destination.port',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.continent_name',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.country_name',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.country_iso_code',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.city_name',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.region_iso_code',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.region_name',
- include_unmapped: true,
- },
- {
- field: 'dns.question.name',
- include_unmapped: true,
- },
- {
- field: 'dns.question.type',
- include_unmapped: true,
- },
- {
- field: 'dns.resolved_ip',
- include_unmapped: true,
- },
- {
- field: 'dns.response_code',
- include_unmapped: true,
- },
- {
- field: 'endgame.exit_code',
- include_unmapped: true,
- },
- {
- field: 'endgame.file_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.file_path',
- include_unmapped: true,
- },
- {
- field: 'endgame.logon_type',
- include_unmapped: true,
- },
- {
- field: 'endgame.parent_process_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.pid',
- include_unmapped: true,
- },
- {
- field: 'endgame.process_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.subject_domain_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.subject_logon_id',
- include_unmapped: true,
- },
- {
- field: 'endgame.subject_user_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.target_domain_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.target_logon_id',
- include_unmapped: true,
- },
- {
- field: 'endgame.target_user_name',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.saved_id',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.timeline_id',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.timeline_title',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.output_index',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.note',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.threshold',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.exceptions_list',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.building_block_type',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.proto',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.flow_id',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.alert.signature',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.alert.signature_id',
- include_unmapped: true,
- },
- {
- field: 'network.bytes',
- include_unmapped: true,
- },
- {
- field: 'network.community_id',
- include_unmapped: true,
- },
- {
- field: 'network.direction',
- include_unmapped: true,
- },
- {
- field: 'network.packets',
- include_unmapped: true,
- },
- {
- field: 'network.protocol',
- include_unmapped: true,
- },
- {
- field: 'network.transport',
- include_unmapped: true,
- },
- {
- field: 'http.version',
- include_unmapped: true,
- },
- {
- field: 'http.request.method',
- include_unmapped: true,
- },
- {
- field: 'http.request.body.bytes',
- include_unmapped: true,
- },
- {
- field: 'http.request.body.content',
- include_unmapped: true,
- },
- {
- field: 'http.request.referrer',
- include_unmapped: true,
- },
- {
- field: 'http.response.status_code',
- include_unmapped: true,
- },
- {
- field: 'http.response.body.bytes',
- include_unmapped: true,
- },
- {
- field: 'http.response.body.content',
- include_unmapped: true,
- },
- {
- field: 'tls.client_certificate.fingerprint.sha1',
- include_unmapped: true,
- },
- {
- field: 'tls.fingerprints.ja3.hash',
- include_unmapped: true,
- },
- {
- field: 'tls.server_certificate.fingerprint.sha1',
- include_unmapped: true,
- },
- {
- field: 'user.domain',
- include_unmapped: true,
- },
- {
- field: 'winlog.event_id',
- include_unmapped: true,
- },
- {
- field: 'process.exit_code',
- include_unmapped: true,
- },
- {
- field: 'process.hash.md5',
- include_unmapped: true,
- },
- {
- field: 'process.hash.sha1',
- include_unmapped: true,
- },
- {
- field: 'process.hash.sha256',
- include_unmapped: true,
- },
- {
- field: 'process.parent.name',
- include_unmapped: true,
- },
- {
- field: 'process.parent.pid',
- include_unmapped: true,
- },
- {
- field: 'process.pid',
- include_unmapped: true,
- },
- {
- field: 'process.name',
- include_unmapped: true,
- },
- {
- field: 'process.ppid',
- include_unmapped: true,
- },
- {
- field: 'process.args',
- include_unmapped: true,
- },
- {
- field: 'process.entity_id',
- include_unmapped: true,
- },
- {
- field: 'process.executable',
- include_unmapped: true,
- },
- {
- field: 'process.title',
- include_unmapped: true,
- },
- {
- field: 'process.working_directory',
- include_unmapped: true,
- },
- {
- field: 'zeek.session_id',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.local_resp',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.local_orig',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.missed_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.state',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.history',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.suppress_for',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.msg',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.note',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.sub',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.dst',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.dropped',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.peer_descr',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.AA',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qclass_name',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.RD',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qtype_name',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qtype',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.query',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.trans_id',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qclass',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.RA',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.TC',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.resp_mime_types',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.trans_depth',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.status_msg',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.resp_fuids',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.tags',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.session_ids',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.timedout',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.local_orig',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.tx_host',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.source',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.is_orig',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.overflow_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.sha1',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.duration',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.depth',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.analyzers',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.mime_type',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.rx_host',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.total_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.fuid',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.seen_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.missing_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.md5',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.cipher',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.established',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.resumed',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.version',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.matched.atomic',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.matched.type',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.event.dataset',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.event.reference',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.provider',
- include_unmapped: true,
- },
- ]);
- });
-
- it('remove internal attributes starting with _', async () => {
- const res = await buildFieldsRequest([
- '@timestamp',
- '_id',
- 'host.name',
- 'destination.ip',
- 'source.ip',
- 'source.geo.location',
- '_type',
- 'threat.indicator.matched.field',
- ]);
- expect(res.some((f) => f.field === '_id')).toEqual(false);
- expect(res.some((f) => f.field === '_type')).toEqual(false);
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json
index bebfd9ca88c23..0df41b9f988b7 100644
--- a/x-pack/plugins/security_solution/tsconfig.json
+++ b/x-pack/plugins/security_solution/tsconfig.json
@@ -42,5 +42,6 @@
{ "path": "../ml/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
{ "path": "../security/tsconfig.json"},
+ { "path": "../timelines/tsconfig.json"},
]
}
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
index fa387ddc151fc..39852ebaeb46b 100644
--- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -31,6 +31,9 @@
"__index": {
"type": "long"
},
+ "__swimlane": {
+ "type": "long"
+ },
"__pagerduty": {
"type": "long"
},
@@ -68,6 +71,9 @@
"__index": {
"type": "long"
},
+ "__swimlane": {
+ "type": "long"
+ },
"__pagerduty": {
"type": "long"
},
@@ -1924,9 +1930,6 @@
"create_first_engine_button": {
"type": "long"
},
- "header_launch_button": {
- "type": "long"
- },
"engine_table_link": {
"type": "long"
}
diff --git a/x-pack/plugins/timelines/README.md b/x-pack/plugins/timelines/README.md
index 441a505903698..0c14953837d02 100644
--- a/x-pack/plugins/timelines/README.md
+++ b/x-pack/plugins/timelines/README.md
@@ -3,9 +3,9 @@ Timelines is a plugin that provides a grid component with accompanying server si
## Using timelines in another plugin
-- Add `TimelinesPluginSetup` to Kibana plugin `SetupServices` dependencies:
+- Add `TimelinesPluginUI` to Kibana plugin `SetupServices` dependencies:
```ts
-timelines: TimelinesPluginSetup;
+timelines: TimelinesPluginUI;
```
- Once `timelines` is added as a required plugin in the consuming plugin's kibana.json, timeline functionality will be available as any other kibana plugin, ie PluginSetupDependencies.timelines.getTimeline()
diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts
new file mode 100644
index 0000000000000..86ff9d501f148
--- /dev/null
+++ b/x-pack/plugins/timelines/common/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
diff --git a/x-pack/plugins/timelines/common/ecs/agent/index.ts b/x-pack/plugins/timelines/common/ecs/agent/index.ts
new file mode 100644
index 0000000000000..2332b60f1a3ca
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/agent/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface AgentEcs {
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/auditd/index.ts b/x-pack/plugins/timelines/common/ecs/auditd/index.ts
new file mode 100644
index 0000000000000..f210f8862dc44
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/auditd/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface AuditdEcs {
+ result?: string[];
+
+ session?: string[];
+
+ data?: AuditdDataEcs;
+
+ summary?: SummaryEcs;
+
+ sequence?: string[];
+}
+
+export interface AuditdDataEcs {
+ acct?: string[];
+
+ terminal?: string[];
+
+ op?: string[];
+}
+
+export interface SummaryEcs {
+ actor?: PrimarySecondaryEcs;
+
+ object?: PrimarySecondaryEcs;
+
+ how?: string[];
+
+ message_type?: string[];
+
+ sequence?: string[];
+}
+
+export interface PrimarySecondaryEcs {
+ primary?: string[];
+
+ secondary?: string[];
+
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/cloud/index.ts b/x-pack/plugins/timelines/common/ecs/cloud/index.ts
new file mode 100644
index 0000000000000..a169e5561c6b6
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/cloud/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface CloudEcs {
+ instance?: CloudInstanceEcs;
+ machine?: CloudMachineEcs;
+ provider?: string[];
+ region?: string[];
+}
+
+export interface CloudMachineEcs {
+ type?: string[];
+}
+
+export interface CloudInstanceEcs {
+ id?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/destination/index.ts b/x-pack/plugins/timelines/common/ecs/destination/index.ts
new file mode 100644
index 0000000000000..2d3b6154276b9
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/destination/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { GeoEcs } from '../geo';
+
+export interface DestinationEcs {
+ bytes?: number[];
+
+ ip?: string[];
+
+ port?: number[];
+
+ domain?: string[];
+
+ geo?: GeoEcs;
+
+ packets?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/dns/index.ts b/x-pack/plugins/timelines/common/ecs/dns/index.ts
new file mode 100644
index 0000000000000..e0f142d9cf57a
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/dns/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface DnsEcs {
+ question?: DnsQuestionEcs;
+
+ resolved_ip?: string[];
+
+ response_code?: string[];
+}
+
+export interface DnsQuestionEcs {
+ name?: string[];
+
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts
new file mode 100644
index 0000000000000..e27b15f021257
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { extendMap } from './extend_map';
+
+describe('ecs_fields test', () => {
+ describe('extendMap', () => {
+ test('it should extend a record', () => {
+ const osFieldsMap: Readonly> = {
+ 'os.platform': 'os.platform',
+ 'os.full': 'os.full',
+ 'os.family': 'os.family',
+ 'os.version': 'os.version',
+ 'os.kernel': 'os.kernel',
+ };
+ const expected: Record = {
+ 'host.os.family': 'host.os.family',
+ 'host.os.full': 'host.os.full',
+ 'host.os.kernel': 'host.os.kernel',
+ 'host.os.platform': 'host.os.platform',
+ 'host.os.version': 'host.os.version',
+ };
+ expect(extendMap('host', osFieldsMap)).toEqual(expected);
+ });
+
+ test('it should extend a sample hosts record', () => {
+ const hostMap: Record = {
+ 'host.id': 'host.id',
+ 'host.ip': 'host.ip',
+ 'host.name': 'host.name',
+ };
+ const osFieldsMap: Readonly> = {
+ 'os.platform': 'os.platform',
+ 'os.full': 'os.full',
+ 'os.family': 'os.family',
+ 'os.version': 'os.version',
+ 'os.kernel': 'os.kernel',
+ };
+ const expected: Record = {
+ 'host.id': 'host.id',
+ 'host.ip': 'host.ip',
+ 'host.name': 'host.name',
+ 'host.os.family': 'host.os.family',
+ 'host.os.full': 'host.os.full',
+ 'host.os.kernel': 'host.os.kernel',
+ 'host.os.platform': 'host.os.platform',
+ 'host.os.version': 'host.os.version',
+ };
+ const output = { ...hostMap, ...extendMap('host', osFieldsMap) };
+ expect(output).toEqual(expected);
+ });
+ });
+});
diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts
new file mode 100644
index 0000000000000..184e6b4f32566
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const extendMap = (
+ path: string,
+ map: Readonly>
+): Readonly> =>
+ Object.entries(map).reduce>((accum, [key, value]) => {
+ accum[`${path}.${key}`] = `${path}.${value}`;
+ return accum;
+ }, {});
diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts
new file mode 100644
index 0000000000000..292822019fc9c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts
@@ -0,0 +1,359 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { extendMap } from './extend_map';
+
+export const auditdMap: Readonly> = {
+ 'auditd.result': 'auditd.result',
+ 'auditd.session': 'auditd.session',
+ 'auditd.data.acct': 'auditd.data.acct',
+ 'auditd.data.terminal': 'auditd.data.terminal',
+ 'auditd.data.op': 'auditd.data.op',
+ 'auditd.summary.actor.primary': 'auditd.summary.actor.primary',
+ 'auditd.summary.actor.secondary': 'auditd.summary.actor.secondary',
+ 'auditd.summary.object.primary': 'auditd.summary.object.primary',
+ 'auditd.summary.object.secondary': 'auditd.summary.object.secondary',
+ 'auditd.summary.object.type': 'auditd.summary.object.type',
+ 'auditd.summary.how': 'auditd.summary.how',
+ 'auditd.summary.message_type': 'auditd.summary.message_type',
+ 'auditd.summary.sequence': 'auditd.summary.sequence',
+};
+
+export const cloudFieldsMap: Readonly> = {
+ 'cloud.account.id': 'cloud.account.id',
+ 'cloud.availability_zone': 'cloud.availability_zone',
+ 'cloud.instance.id': 'cloud.instance.id',
+ 'cloud.instance.name': 'cloud.instance.name',
+ 'cloud.machine.type': 'cloud.machine.type',
+ 'cloud.provider': 'cloud.provider',
+ 'cloud.region': 'cloud.region',
+};
+
+export const fileMap: Readonly> = {
+ 'file.name': 'file.name',
+ 'file.path': 'file.path',
+ 'file.target_path': 'file.target_path',
+ 'file.extension': 'file.extension',
+ 'file.type': 'file.type',
+ 'file.device': 'file.device',
+ 'file.inode': 'file.inode',
+ 'file.uid': 'file.uid',
+ 'file.owner': 'file.owner',
+ 'file.gid': 'file.gid',
+ 'file.group': 'file.group',
+ 'file.mode': 'file.mode',
+ 'file.size': 'file.size',
+ 'file.mtime': 'file.mtime',
+ 'file.ctime': 'file.ctime',
+};
+
+export const osFieldsMap: Readonly> = {
+ 'os.platform': 'os.platform',
+ 'os.name': 'os.name',
+ 'os.full': 'os.full',
+ 'os.family': 'os.family',
+ 'os.version': 'os.version',
+ 'os.kernel': 'os.kernel',
+};
+
+export const hostFieldsMap: Readonly> = {
+ 'host.architecture': 'host.architecture',
+ 'host.id': 'host.id',
+ 'host.ip': 'host.ip',
+ 'host.mac': 'host.mac',
+ 'host.name': 'host.name',
+ ...extendMap('host', osFieldsMap),
+};
+
+export const processFieldsMap: Readonly> = {
+ 'process.hash.md5': 'process.hash.md5',
+ 'process.hash.sha1': 'process.hash.sha1',
+ 'process.hash.sha256': 'process.hash.sha256',
+ 'process.pid': 'process.pid',
+ 'process.name': 'process.name',
+ 'process.ppid': 'process.ppid',
+ 'process.args': 'process.args',
+ 'process.entity_id': 'process.entity_id',
+ 'process.executable': 'process.executable',
+ 'process.title': 'process.title',
+ 'process.thread': 'process.thread',
+ 'process.working_directory': 'process.working_directory',
+};
+
+export const agentFieldsMap: Readonly> = {
+ 'agent.type': 'agent.type',
+};
+
+export const userFieldsMap: Readonly> = {
+ 'user.domain': 'user.domain',
+ 'user.id': 'user.id',
+ 'user.name': 'user.name',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.full_name': 'user.full_name',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.email': 'user.email',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.hash': 'user.hash',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.group': 'user.group',
+};
+
+export const winlogFieldsMap: Readonly> = {
+ 'winlog.event_id': 'winlog.event_id',
+};
+
+export const suricataFieldsMap: Readonly> = {
+ 'suricata.eve.flow_id': 'suricata.eve.flow_id',
+ 'suricata.eve.proto': 'suricata.eve.proto',
+ 'suricata.eve.alert.signature': 'suricata.eve.alert.signature',
+ 'suricata.eve.alert.signature_id': 'suricata.eve.alert.signature_id',
+};
+
+export const tlsFieldsMap: Readonly> = {
+ 'tls.client_certificate.fingerprint.sha1': 'tls.client_certificate.fingerprint.sha1',
+ 'tls.fingerprints.ja3.hash': 'tls.fingerprints.ja3.hash',
+ 'tls.server_certificate.fingerprint.sha1': 'tls.server_certificate.fingerprint.sha1',
+};
+
+export const urlFieldsMap: Readonly> = {
+ 'url.original': 'url.original',
+ 'url.domain': 'url.domain',
+ 'user.username': 'user.username',
+ 'user.password': 'user.password',
+};
+
+export const httpFieldsMap: Readonly> = {
+ 'http.version': 'http.version',
+ 'http.request': 'http.request',
+ 'http.request.method': 'http.request.method',
+ 'http.request.body.bytes': 'http.request.body.bytes',
+ 'http.request.body.content': 'http.request.body.content',
+ 'http.request.referrer': 'http.request.referrer',
+ 'http.response.status_code': 'http.response.status_code',
+ 'http.response.body': 'http.response.body',
+ 'http.response.body.bytes': 'http.response.body.bytes',
+ 'http.response.body.content': 'http.response.body.content',
+};
+
+export const zeekFieldsMap: Readonly> = {
+ 'zeek.session_id': 'zeek.session_id',
+ 'zeek.connection.local_resp': 'zeek.connection.local_resp',
+ 'zeek.connection.local_orig': 'zeek.connection.local_orig',
+ 'zeek.connection.missed_bytes': 'zeek.connection.missed_bytes',
+ 'zeek.connection.state': 'zeek.connection.state',
+ 'zeek.connection.history': 'zeek.connection.history',
+ 'zeek.notice.suppress_for': 'zeek.notice.suppress_for',
+ 'zeek.notice.msg': 'zeek.notice.msg',
+ 'zeek.notice.note': 'zeek.notice.note',
+ 'zeek.notice.sub': 'zeek.notice.sub',
+ 'zeek.notice.dst': 'zeek.notice.dst',
+ 'zeek.notice.dropped': 'zeek.notice.dropped',
+ 'zeek.notice.peer_descr': 'zeek.notice.peer_descr',
+ 'zeek.dns.AA': 'zeek.dns.AA',
+ 'zeek.dns.qclass_name': 'zeek.dns.qclass_name',
+ 'zeek.dns.RD': 'zeek.dns.RD',
+ 'zeek.dns.qtype_name': 'zeek.dns.qtype_name',
+ 'zeek.dns.qtype': 'zeek.dns.qtype',
+ 'zeek.dns.query': 'zeek.dns.query',
+ 'zeek.dns.trans_id': 'zeek.dns.trans_id',
+ 'zeek.dns.qclass': 'zeek.dns.qclass',
+ 'zeek.dns.RA': 'zeek.dns.RA',
+ 'zeek.dns.TC': 'zeek.dns.TC',
+ 'zeek.http.resp_mime_types': 'zeek.http.resp_mime_types',
+ 'zeek.http.trans_depth': 'zeek.http.trans_depth',
+ 'zeek.http.status_msg': 'zeek.http.status_msg',
+ 'zeek.http.resp_fuids': 'zeek.http.resp_fuids',
+ 'zeek.http.tags': 'zeek.http.tags',
+ 'zeek.files.session_ids': 'zeek.files.session_ids',
+ 'zeek.files.timedout': 'zeek.files.timedout',
+ 'zeek.files.local_orig': 'zeek.files.local_orig',
+ 'zeek.files.tx_host': 'zeek.files.tx_host',
+ 'zeek.files.source': 'zeek.files.source',
+ 'zeek.files.is_orig': 'zeek.files.is_orig',
+ 'zeek.files.overflow_bytes': 'zeek.files.overflow_bytes',
+ 'zeek.files.sha1': 'zeek.files.sha1',
+ 'zeek.files.duration': 'zeek.files.duration',
+ 'zeek.files.depth': 'zeek.files.depth',
+ 'zeek.files.analyzers': 'zeek.files.analyzers',
+ 'zeek.files.mime_type': 'zeek.files.mime_type',
+ 'zeek.files.rx_host': 'zeek.files.rx_host',
+ 'zeek.files.total_bytes': 'zeek.files.total_bytes',
+ 'zeek.files.fuid': 'zeek.files.fuid',
+ 'zeek.files.seen_bytes': 'zeek.files.seen_bytes',
+ 'zeek.files.missing_bytes': 'zeek.files.missing_bytes',
+ 'zeek.files.md5': 'zeek.files.md5',
+ 'zeek.ssl.cipher': 'zeek.ssl.cipher',
+ 'zeek.ssl.established': 'zeek.ssl.established',
+ 'zeek.ssl.resumed': 'zeek.ssl.resumed',
+ 'zeek.ssl.version': 'zeek.ssl.version',
+};
+
+export const sourceFieldsMap: Readonly> = {
+ 'source.bytes': 'source.bytes',
+ 'source.ip': 'source.ip',
+ 'source.packets': 'source.packets',
+ 'source.port': 'source.port',
+ 'source.domain': 'source.domain',
+ 'source.geo.continent_name': 'source.geo.continent_name',
+ 'source.geo.country_name': 'source.geo.country_name',
+ 'source.geo.country_iso_code': 'source.geo.country_iso_code',
+ 'source.geo.city_name': 'source.geo.city_name',
+ 'source.geo.region_iso_code': 'source.geo.region_iso_code',
+ 'source.geo.region_name': 'source.geo.region_name',
+};
+
+export const destinationFieldsMap: Readonly> = {
+ 'destination.bytes': 'destination.bytes',
+ 'destination.ip': 'destination.ip',
+ 'destination.packets': 'destination.packets',
+ 'destination.port': 'destination.port',
+ 'destination.domain': 'destination.domain',
+ 'destination.geo.continent_name': 'destination.geo.continent_name',
+ 'destination.geo.country_name': 'destination.geo.country_name',
+ 'destination.geo.country_iso_code': 'destination.geo.country_iso_code',
+ 'destination.geo.city_name': 'destination.geo.city_name',
+ 'destination.geo.region_iso_code': 'destination.geo.region_iso_code',
+ 'destination.geo.region_name': 'destination.geo.region_name',
+};
+
+export const networkFieldsMap: Readonly> = {
+ 'network.bytes': 'network.bytes',
+ 'network.community_id': 'network.community_id',
+ 'network.direction': 'network.direction',
+ 'network.packets': 'network.packets',
+ 'network.protocol': 'network.protocol',
+ 'network.transport': 'network.transport',
+};
+
+export const geoFieldsMap: Readonly> = {
+ 'geo.region_name': 'destination.geo.region_name',
+ 'geo.country_iso_code': 'destination.geo.country_iso_code',
+};
+
+export const dnsFieldsMap: Readonly> = {
+ 'dns.question.name': 'dns.question.name',
+ 'dns.question.type': 'dns.question.type',
+ 'dns.resolved_ip': 'dns.resolved_ip',
+ 'dns.response_code': 'dns.response_code',
+};
+
+export const endgameFieldsMap: Readonly> = {
+ 'endgame.exit_code': 'endgame.exit_code',
+ 'endgame.file_name': 'endgame.file_name',
+ 'endgame.file_path': 'endgame.file_path',
+ 'endgame.logon_type': 'endgame.logon_type',
+ 'endgame.parent_process_name': 'endgame.parent_process_name',
+ 'endgame.pid': 'endgame.pid',
+ 'endgame.process_name': 'endgame.process_name',
+ 'endgame.subject_domain_name': 'endgame.subject_domain_name',
+ 'endgame.subject_logon_id': 'endgame.subject_logon_id',
+ 'endgame.subject_user_name': 'endgame.subject_user_name',
+ 'endgame.target_domain_name': 'endgame.target_domain_name',
+ 'endgame.target_logon_id': 'endgame.target_logon_id',
+ 'endgame.target_user_name': 'endgame.target_user_name',
+};
+
+export const eventBaseFieldsMap: Readonly> = {
+ 'event.action': 'event.action',
+ 'event.category': 'event.category',
+ 'event.code': 'event.code',
+ 'event.created': 'event.created',
+ 'event.dataset': 'event.dataset',
+ 'event.duration': 'event.duration',
+ 'event.end': 'event.end',
+ 'event.hash': 'event.hash',
+ 'event.id': 'event.id',
+ 'event.kind': 'event.kind',
+ 'event.module': 'event.module',
+ 'event.original': 'event.original',
+ 'event.outcome': 'event.outcome',
+ 'event.risk_score': 'event.risk_score',
+ 'event.risk_score_norm': 'event.risk_score_norm',
+ 'event.severity': 'event.severity',
+ 'event.start': 'event.start',
+ 'event.timezone': 'event.timezone',
+ 'event.type': 'event.type',
+};
+
+export const systemFieldsMap: Readonly> = {
+ 'system.audit.package.arch': 'system.audit.package.arch',
+ 'system.audit.package.entity_id': 'system.audit.package.entity_id',
+ 'system.audit.package.name': 'system.audit.package.name',
+ 'system.audit.package.size': 'system.audit.package.size',
+ 'system.audit.package.summary': 'system.audit.package.summary',
+ 'system.audit.package.version': 'system.audit.package.version',
+ 'system.auth.ssh.signature': 'system.auth.ssh.signature',
+ 'system.auth.ssh.method': 'system.auth.ssh.method',
+};
+
+export const signalFieldsMap: Readonly> = {
+ 'signal.original_time': 'signal.original_time',
+ 'signal.rule.id': 'signal.rule.id',
+ 'signal.rule.saved_id': 'signal.rule.saved_id',
+ 'signal.rule.timeline_id': 'signal.rule.timeline_id',
+ 'signal.rule.timeline_title': 'signal.rule.timeline_title',
+ 'signal.rule.output_index': 'signal.rule.output_index',
+ 'signal.rule.from': 'signal.rule.from',
+ 'signal.rule.index': 'signal.rule.index',
+ 'signal.rule.language': 'signal.rule.language',
+ 'signal.rule.query': 'signal.rule.query',
+ 'signal.rule.to': 'signal.rule.to',
+ 'signal.rule.filters': 'signal.rule.filters',
+ 'signal.rule.rule_id': 'signal.rule.rule_id',
+ 'signal.rule.false_positives': 'signal.rule.false_positives',
+ 'signal.rule.max_signals': 'signal.rule.max_signals',
+ 'signal.rule.risk_score': 'signal.rule.risk_score',
+ 'signal.rule.description': 'signal.rule.description',
+ 'signal.rule.name': 'signal.rule.name',
+ 'signal.rule.immutable': 'signal.rule.immutable',
+ 'signal.rule.references': 'signal.rule.references',
+ 'signal.rule.severity': 'signal.rule.severity',
+ 'signal.rule.tags': 'signal.rule.tags',
+ 'signal.rule.threat': 'signal.rule.threat',
+ 'signal.rule.type': 'signal.rule.type',
+ 'signal.rule.size': 'signal.rule.size',
+ 'signal.rule.enabled': 'signal.rule.enabled',
+ 'signal.rule.created_at': 'signal.rule.created_at',
+ 'signal.rule.updated_at': 'signal.rule.updated_at',
+ 'signal.rule.created_by': 'signal.rule.created_by',
+ 'signal.rule.updated_by': 'signal.rule.updated_by',
+ 'signal.rule.version': 'signal.rule.version',
+ 'signal.rule.note': 'signal.rule.note',
+ 'signal.rule.threshold': 'signal.rule.threshold',
+ 'signal.rule.exceptions_list': 'signal.rule.exceptions_list',
+};
+
+export const ruleFieldsMap: Readonly> = {
+ 'rule.reference': 'rule.reference',
+};
+
+export const eventFieldsMap: Readonly> = {
+ timestamp: '@timestamp',
+ '@timestamp': '@timestamp',
+ message: 'message',
+ ...{ ...agentFieldsMap },
+ ...{ ...auditdMap },
+ ...{ ...destinationFieldsMap },
+ ...{ ...dnsFieldsMap },
+ ...{ ...endgameFieldsMap },
+ ...{ ...eventBaseFieldsMap },
+ ...{ ...fileMap },
+ ...{ ...geoFieldsMap },
+ ...{ ...hostFieldsMap },
+ ...{ ...networkFieldsMap },
+ ...{ ...ruleFieldsMap },
+ ...{ ...signalFieldsMap },
+ ...{ ...sourceFieldsMap },
+ ...{ ...suricataFieldsMap },
+ ...{ ...systemFieldsMap },
+ ...{ ...tlsFieldsMap },
+ ...{ ...zeekFieldsMap },
+ ...{ ...httpFieldsMap },
+ ...{ ...userFieldsMap },
+ ...{ ...winlogFieldsMap },
+ ...{ ...processFieldsMap },
+};
diff --git a/x-pack/plugins/timelines/common/ecs/endgame/index.ts b/x-pack/plugins/timelines/common/ecs/endgame/index.ts
new file mode 100644
index 0000000000000..f82a9587c75c3
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/endgame/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface EndgameEcs {
+ exit_code?: number[];
+ file_name?: string[];
+ file_path?: string[];
+ logon_type?: number[];
+ parent_process_name?: string[];
+ pid?: number[];
+ process_name?: string[];
+ subject_domain_name?: string[];
+ subject_logon_id?: string[];
+ subject_user_name?: string[];
+ target_domain_name?: string[];
+ target_logon_id?: string[];
+ target_user_name?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/event/index.ts b/x-pack/plugins/timelines/common/ecs/event/index.ts
new file mode 100644
index 0000000000000..4e38bacefd351
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/event/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface EventEcs {
+ action?: string[];
+
+ category?: string[];
+
+ code?: string[];
+
+ created?: string[];
+
+ dataset?: string[];
+
+ duration?: number[];
+
+ end?: string[];
+
+ hash?: string[];
+
+ id?: string[];
+
+ kind?: string[];
+
+ module?: string[];
+
+ original?: string[];
+
+ outcome?: string[];
+
+ risk_score?: number[];
+
+ risk_score_norm?: number[];
+
+ severity?: number[];
+
+ start?: string[];
+
+ timezone?: string[];
+
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/file/index.ts b/x-pack/plugins/timelines/common/ecs/file/index.ts
new file mode 100644
index 0000000000000..5e409b1095cf5
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/file/index.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+interface Original {
+ name?: string[];
+ path?: string[];
+}
+
+export interface CodeSignature {
+ subject_name: string[];
+ trusted: string[];
+}
+export interface Ext {
+ code_signature?: CodeSignature[] | CodeSignature;
+ original?: Original;
+}
+export interface Hash {
+ md5?: string[];
+ sha1?: string[];
+ sha256: string[];
+}
+
+export interface FileEcs {
+ name?: string[];
+
+ path?: string[];
+
+ target_path?: string[];
+
+ extension?: string[];
+
+ Ext?: Ext;
+
+ type?: string[];
+
+ device?: string[];
+
+ inode?: string[];
+
+ uid?: string[];
+
+ owner?: string[];
+
+ gid?: string[];
+
+ group?: string[];
+
+ mode?: string[];
+
+ size?: number[];
+
+ mtime?: string[];
+
+ ctime?: string[];
+
+ hash?: Hash;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/geo/index.ts b/x-pack/plugins/timelines/common/ecs/geo/index.ts
new file mode 100644
index 0000000000000..b6bf0f7b8aaad
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/geo/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface GeoEcs {
+ city_name?: string[];
+ continent_name?: string[];
+ country_iso_code?: string[];
+ country_name?: string[];
+ location?: Location;
+ region_iso_code?: string[];
+ region_name?: string[];
+}
+
+export interface Location {
+ lon?: number[];
+ lat?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/host/index.ts b/x-pack/plugins/timelines/common/ecs/host/index.ts
new file mode 100644
index 0000000000000..37032c91fc312
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/host/index.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface HostEcs {
+ architecture?: string[];
+
+ id?: string[];
+
+ ip?: string[];
+
+ mac?: string[];
+
+ name?: string[];
+
+ os?: OsEcs;
+
+ type?: string[];
+}
+
+export interface OsEcs {
+ platform?: string[];
+
+ name?: string[];
+
+ full?: string[];
+
+ family?: string[];
+
+ version?: string[];
+
+ kernel?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/http/index.ts b/x-pack/plugins/timelines/common/ecs/http/index.ts
new file mode 100644
index 0000000000000..89ce6b678181b
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/http/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface HttpEcs {
+ version?: string[];
+
+ request?: HttpRequestData;
+
+ response?: HttpResponseData;
+}
+
+export interface HttpRequestData {
+ method?: string[];
+
+ body?: HttpBodyData;
+
+ referrer?: string[];
+
+ bytes?: number[];
+}
+
+export interface HttpBodyData {
+ content?: string[];
+
+ bytes?: number[];
+}
+
+export interface HttpResponseData {
+ status_code?: number[];
+
+ body?: HttpBodyData;
+
+ bytes?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/index.ts b/x-pack/plugins/timelines/common/ecs/index.ts
new file mode 100644
index 0000000000000..8054b3c8521db
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AgentEcs } from './agent';
+import { AuditdEcs } from './auditd';
+import { DestinationEcs } from './destination';
+import { DnsEcs } from './dns';
+import { EndgameEcs } from './endgame';
+import { EventEcs } from './event';
+import { FileEcs } from './file';
+import { GeoEcs } from './geo';
+import { HostEcs } from './host';
+import { NetworkEcs } from './network';
+import { RegistryEcs } from './registry';
+import { RuleEcs } from './rule';
+import { SignalEcs } from './signal';
+import { SourceEcs } from './source';
+import { SuricataEcs } from './suricata';
+import { TlsEcs } from './tls';
+import { ZeekEcs } from './zeek';
+import { HttpEcs } from './http';
+import { UrlEcs } from './url';
+import { UserEcs } from './user';
+import { WinlogEcs } from './winlog';
+import { ProcessEcs } from './process';
+import { SystemEcs } from './system';
+import { ThreatEcs } from './threat';
+import { Ransomware } from './ransomware';
+
+export interface Ecs {
+ _id: string;
+ _index?: string;
+ agent?: AgentEcs;
+ auditd?: AuditdEcs;
+ destination?: DestinationEcs;
+ dns?: DnsEcs;
+ endgame?: EndgameEcs;
+ event?: EventEcs;
+ geo?: GeoEcs;
+ host?: HostEcs;
+ network?: NetworkEcs;
+ registry?: RegistryEcs;
+ rule?: RuleEcs;
+ signal?: SignalEcs;
+ source?: SourceEcs;
+ suricata?: SuricataEcs;
+ tls?: TlsEcs;
+ zeek?: ZeekEcs;
+ http?: HttpEcs;
+ url?: UrlEcs;
+ timestamp?: string;
+ message?: string[];
+ user?: UserEcs;
+ winlog?: WinlogEcs;
+ process?: ProcessEcs;
+ file?: FileEcs;
+ system?: SystemEcs;
+ threat?: ThreatEcs;
+ // This should be temporary
+ eql?: { parentId: string; sequenceNumber: string };
+ Ransomware?: Ransomware;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/network/index.ts b/x-pack/plugins/timelines/common/ecs/network/index.ts
new file mode 100644
index 0000000000000..6cc5dacab1e53
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/network/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface NetworkEcs {
+ bytes?: number[];
+ community_id?: string[];
+ direction?: string[];
+ packets?: number[];
+ protocol?: string[];
+ transport?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/process/index.ts b/x-pack/plugins/timelines/common/ecs/process/index.ts
new file mode 100644
index 0000000000000..820ecc5560e6c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/process/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Ext } from '../file';
+
+export interface ProcessEcs {
+ Ext?: Ext;
+ entity_id?: string[];
+ exit_code?: number[];
+ hash?: ProcessHashData;
+ parent?: ProcessParentData;
+ pid?: number[];
+ name?: string[];
+ ppid?: number[];
+ args?: string[];
+ executable?: string[];
+ title?: string[];
+ thread?: Thread;
+ working_directory?: string[];
+}
+
+export interface ProcessHashData {
+ md5?: string[];
+ sha1?: string[];
+ sha256?: string[];
+}
+
+export interface ProcessParentData {
+ name?: string[];
+ pid?: number[];
+}
+
+export interface Thread {
+ id?: number[];
+ start?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/ransomware/index.ts b/x-pack/plugins/timelines/common/ecs/ransomware/index.ts
new file mode 100644
index 0000000000000..1724a264f8a4c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ransomware/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface Ransomware {
+ feature?: string[];
+ score?: string[];
+ version?: number[];
+ child_pids?: string[];
+ files?: RansomwareFiles;
+}
+
+export interface RansomwareFiles {
+ operation?: string[];
+ entropy?: number[];
+ metrics?: string[];
+ extension?: string[];
+ original?: OriginalRansomwareFiles;
+ path?: string[];
+ data?: string[];
+ score?: number[];
+}
+
+export interface OriginalRansomwareFiles {
+ path?: string[];
+ extension?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/registry/index.ts b/x-pack/plugins/timelines/common/ecs/registry/index.ts
new file mode 100644
index 0000000000000..c756fb139199e
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/registry/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface RegistryEcs {
+ hive?: string[];
+ key?: string[];
+ path?: string[];
+ value?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/rule/index.ts b/x-pack/plugins/timelines/common/ecs/rule/index.ts
new file mode 100644
index 0000000000000..ae7e5064a8ece
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/rule/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface RuleEcs {
+ id?: string[];
+ rule_id?: string[];
+ name?: string[];
+ false_positives?: string[];
+ saved_id?: string[];
+ timeline_id?: string[];
+ timeline_title?: string[];
+ max_signals?: number[];
+ risk_score?: string[];
+ output_index?: string[];
+ description?: string[];
+ from?: string[];
+ immutable?: boolean[];
+ index?: string[];
+ interval?: string[];
+ language?: string[];
+ query?: string[];
+ references?: string[];
+ severity?: string[];
+ tags?: string[];
+ threat?: unknown;
+ threshold?: unknown;
+ type?: string[];
+ size?: string[];
+ to?: string[];
+ enabled?: boolean[];
+ filters?: unknown;
+ created_at?: string[];
+ updated_at?: string[];
+ created_by?: string[];
+ updated_by?: string[];
+ version?: string[];
+ note?: string[];
+ building_block_type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/signal/index.ts b/x-pack/plugins/timelines/common/ecs/signal/index.ts
new file mode 100644
index 0000000000000..45e1f04d2b405
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/signal/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { RuleEcs } from '../rule';
+
+export interface SignalEcs {
+ rule?: RuleEcs;
+ original_time?: string[];
+ status?: string[];
+ group?: {
+ id?: string[];
+ };
+ threshold_result?: unknown;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/source/index.ts b/x-pack/plugins/timelines/common/ecs/source/index.ts
new file mode 100644
index 0000000000000..10a2025eb43ec
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/source/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { GeoEcs } from '../geo';
+
+export interface SourceEcs {
+ bytes?: number[];
+ ip?: string[];
+ port?: number[];
+ domain?: string[];
+ geo?: GeoEcs;
+ packets?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/suricata/index.ts b/x-pack/plugins/timelines/common/ecs/suricata/index.ts
new file mode 100644
index 0000000000000..5555a40188432
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/suricata/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface SuricataEcs {
+ eve?: SuricataEveData;
+}
+
+export interface SuricataEveData {
+ alert?: SuricataAlertData;
+
+ flow_id?: number[];
+
+ proto?: string[];
+}
+
+export interface SuricataAlertData {
+ signature?: string[];
+
+ signature_id?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/system/index.ts b/x-pack/plugins/timelines/common/ecs/system/index.ts
new file mode 100644
index 0000000000000..f2313c7884511
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/system/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface SystemEcs {
+ audit?: AuditEcs;
+
+ auth?: AuthEcs;
+}
+
+export interface AuditEcs {
+ package?: PackageEcs;
+}
+
+export interface PackageEcs {
+ arch?: string[];
+
+ entity_id?: string[];
+
+ name?: string[];
+
+ size?: number[];
+
+ summary?: string[];
+
+ version?: string[];
+}
+
+export interface AuthEcs {
+ ssh?: SshEcs;
+}
+
+export interface SshEcs {
+ method?: string[];
+
+ signature?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/threat/index.ts b/x-pack/plugins/timelines/common/ecs/threat/index.ts
new file mode 100644
index 0000000000000..19923a82dc846
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/threat/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EventEcs } from '../event';
+
+interface ThreatMatchEcs {
+ atomic?: string[];
+ field?: string[];
+ type?: string[];
+}
+
+export interface ThreatIndicatorEcs {
+ matched?: ThreatMatchEcs;
+ event?: EventEcs & { reference?: string[] };
+ provider?: string[];
+ type?: string[];
+}
+
+export interface ThreatEcs {
+ indicator: ThreatIndicatorEcs[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/tls/index.ts b/x-pack/plugins/timelines/common/ecs/tls/index.ts
new file mode 100644
index 0000000000000..f2e6b3d36653d
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/tls/index.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface TlsEcs {
+ client_certificate?: TlsClientCertificateData;
+
+ fingerprints?: TlsFingerprintsData;
+
+ server_certificate?: TlsServerCertificateData;
+}
+
+export interface TlsClientCertificateData {
+ fingerprint?: FingerprintData;
+}
+
+export interface FingerprintData {
+ sha1?: string[];
+}
+
+export interface TlsFingerprintsData {
+ ja3?: TlsJa3Data;
+}
+
+export interface TlsJa3Data {
+ hash?: string[];
+}
+
+export interface TlsServerCertificateData {
+ fingerprint?: FingerprintData;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/url/index.ts b/x-pack/plugins/timelines/common/ecs/url/index.ts
new file mode 100644
index 0000000000000..ea9dc303108e3
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/url/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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface UrlEcs {
+ domain?: string[];
+
+ original?: string[];
+
+ username?: string[];
+
+ password?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/user/index.ts b/x-pack/plugins/timelines/common/ecs/user/index.ts
new file mode 100644
index 0000000000000..b03a8e5e96b41
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/user/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface UserEcs {
+ domain?: string[];
+
+ id?: string[];
+
+ name?: string[];
+
+ full_name?: string[];
+
+ email?: string[];
+
+ hash?: string[];
+
+ group?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/winlog/index.ts b/x-pack/plugins/timelines/common/ecs/winlog/index.ts
new file mode 100644
index 0000000000000..27757d05ba6ec
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/winlog/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface WinlogEcs {
+ event_id?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/zeek/index.ts b/x-pack/plugins/timelines/common/ecs/zeek/index.ts
new file mode 100644
index 0000000000000..b1a3786ae74aa
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/zeek/index.ts
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface ZeekEcs {
+ session_id?: string[];
+
+ connection?: ZeekConnectionData;
+
+ notice?: ZeekNoticeData;
+
+ dns?: ZeekDnsData;
+
+ http?: ZeekHttpData;
+
+ files?: ZeekFileData;
+
+ ssl?: ZeekSslData;
+}
+
+export interface ZeekConnectionData {
+ local_resp?: boolean[];
+
+ local_orig?: boolean[];
+
+ missed_bytes?: number[];
+
+ state?: string[];
+
+ history?: string[];
+}
+
+export interface ZeekNoticeData {
+ suppress_for?: number[];
+
+ msg?: string[];
+
+ note?: string[];
+
+ sub?: string[];
+
+ dst?: string[];
+
+ dropped?: boolean[];
+
+ peer_descr?: string[];
+}
+
+export interface ZeekDnsData {
+ AA?: boolean[];
+
+ qclass_name?: string[];
+
+ RD?: boolean[];
+
+ qtype_name?: string[];
+
+ rejected?: boolean[];
+
+ qtype?: string[];
+
+ query?: string[];
+
+ trans_id?: number[];
+
+ qclass?: string[];
+
+ RA?: boolean[];
+
+ TC?: boolean[];
+}
+
+export interface ZeekHttpData {
+ resp_mime_types?: string[];
+
+ trans_depth?: string[];
+
+ status_msg?: string[];
+
+ resp_fuids?: string[];
+
+ tags?: string[];
+}
+
+export interface ZeekFileData {
+ session_ids?: string[];
+
+ timedout?: boolean[];
+
+ local_orig?: boolean[];
+
+ tx_host?: string[];
+
+ source?: string[];
+
+ is_orig?: boolean[];
+
+ overflow_bytes?: number[];
+
+ sha1?: string[];
+
+ duration?: number[];
+
+ depth?: number[];
+
+ analyzers?: string[];
+
+ mime_type?: string[];
+
+ rx_host?: string[];
+
+ total_bytes?: number[];
+
+ fuid?: string[];
+
+ seen_bytes?: number[];
+
+ missing_bytes?: number[];
+
+ md5?: string[];
+}
+
+export interface ZeekSslData {
+ cipher?: string[];
+
+ established?: boolean[];
+
+ resumed?: boolean[];
+
+ version?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts
index c095b6c89627e..05174235c20db 100644
--- a/x-pack/plugins/timelines/common/index.ts
+++ b/x-pack/plugins/timelines/common/index.ts
@@ -5,5 +5,9 @@
* 2.0.
*/
+export * from './types';
+export * from './search_strategy';
+export * from './utils/accessibility';
+
export const PLUGIN_ID = 'timelines';
export const PLUGIN_NAME = 'timelines';
diff --git a/x-pack/plugins/timelines/common/search_strategy/common/index.ts b/x-pack/plugins/timelines/common/search_strategy/common/index.ts
new file mode 100644
index 0000000000000..62c2187e267fa
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/common/index.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import type { estypes } from '@elastic/elasticsearch';
+
+export type Maybe = T | null;
+
+export interface TotalValue {
+ value: number;
+ relation: string;
+}
+
+export interface CursorType {
+ value?: Maybe;
+ tiebreaker?: Maybe;
+}
+
+export interface Inspect {
+ dsl: string[];
+}
+
+export enum Direction {
+ asc = 'asc',
+ desc = 'desc',
+}
+
+export interface SortField {
+ field: Field;
+ direction: Direction;
+}
+
+export interface TimerangeInput {
+ /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
+ interval: string;
+ /** The end of the timerange */
+ to: string;
+ /** The beginning of the timerange */
+ from: string;
+}
+
+export interface PaginationInputPaginated {
+ /** The activePage parameter defines the page of results you want to fetch */
+ activePage: number;
+ /** The cursorStart parameter defines the start of the results to be displayed */
+ cursorStart: number;
+ /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
+ fakePossibleCount: number;
+ /** The querySize parameter is the number of items to be returned */
+ querySize: number;
+}
+
+export type DocValueFields = estypes.SearchDocValueField;
+
+export interface TimerangeFilter {
+ range: {
+ [timestamp: string]: {
+ gte: string;
+ lte: string;
+ format: string;
+ };
+ };
+}
+
+export interface Fields {
+ [x: string]: T | Array>;
+}
+
+export interface EventSource {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [field: string]: any;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export interface EventHit extends estypes.SearchHit> {
+ sort: string[];
+ fields: Fields;
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/index.ts b/x-pack/plugins/timelines/common/search_strategy/eql/index.ts
new file mode 100644
index 0000000000000..4a361bed64890
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/index.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TotalValue } from '../common';
+
+export * from './validation';
+
+export type SearchTypes =
+ | string
+ | string[]
+ | number
+ | number[]
+ | boolean
+ | boolean[]
+ | object
+ | object[]
+ | undefined;
+
+export interface BaseHit {
+ _index: string;
+ _id: string;
+ _source: T;
+ fields?: Record;
+}
+
+export interface EqlSequence {
+ join_keys: SearchTypes[];
+ events: Array>;
+}
+
+export interface EqlSearchResponse {
+ is_partial: boolean;
+ is_running: boolean;
+ took: number;
+ timed_out: boolean;
+ hits: {
+ total: TotalValue;
+ sequences?: Array>;
+ events?: Array>;
+ };
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts
new file mode 100644
index 0000000000000..b3a2c9c9a3f62
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ApiResponse } from '@elastic/elasticsearch';
+import { ErrorResponse } from './helpers';
+
+export const getValidEqlResponse = (): ApiResponse['body'] => ({
+ is_partial: false,
+ is_running: false,
+ took: 162,
+ timed_out: false,
+ hits: {
+ total: {
+ value: 1,
+ relation: 'eq',
+ },
+ sequences: [],
+ },
+});
+
+export const getEqlResponseWithValidationError = (): ErrorResponse => ({
+ error: {
+ root_cause: [
+ {
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+ ],
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+});
+
+export const getEqlResponseWithValidationErrors = (): ErrorResponse => ({
+ error: {
+ root_cause: [
+ {
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+ {
+ type: 'parsing_exception',
+ reason: "line 1:4: mismatched input '' expecting 'where'",
+ },
+ ],
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+});
+
+export const getEqlResponseWithNonValidationError = (): ApiResponse['body'] => ({
+ error: {
+ root_cause: [
+ {
+ type: 'other_error',
+ reason: 'some other reason',
+ },
+ ],
+ type: 'other_error',
+ reason: 'some other reason',
+ },
+});
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts
new file mode 100644
index 0000000000000..de75cf6ac6dc7
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getValidationErrors, isErrorResponse, isValidationErrorResponse } from './helpers';
+import {
+ getEqlResponseWithNonValidationError,
+ getEqlResponseWithValidationError,
+ getEqlResponseWithValidationErrors,
+ getValidEqlResponse,
+} from './helpers.mock';
+
+describe('eql validation helpers', () => {
+ describe('isErrorResponse', () => {
+ it('is false for a regular response', () => {
+ expect(isErrorResponse(getValidEqlResponse())).toEqual(false);
+ });
+
+ it('is true for a response with non-validation errors', () => {
+ expect(isErrorResponse(getEqlResponseWithNonValidationError())).toEqual(true);
+ });
+
+ it('is true for a response with validation errors', () => {
+ expect(isErrorResponse(getEqlResponseWithValidationError())).toEqual(true);
+ });
+ });
+
+ describe('isValidationErrorResponse', () => {
+ it('is false for a regular response', () => {
+ expect(isValidationErrorResponse(getValidEqlResponse())).toEqual(false);
+ });
+
+ it('is false for a response with non-validation errors', () => {
+ expect(isValidationErrorResponse(getEqlResponseWithNonValidationError())).toEqual(false);
+ });
+
+ it('is true for a response with validation errors', () => {
+ expect(isValidationErrorResponse(getEqlResponseWithValidationError())).toEqual(true);
+ });
+ });
+
+ describe('getValidationErrors', () => {
+ it('returns a single error for a single root cause', () => {
+ expect(getValidationErrors(getEqlResponseWithValidationError())).toEqual([
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ ]);
+ });
+
+ it('returns multiple errors for multiple root causes', () => {
+ expect(getValidationErrors(getEqlResponseWithValidationErrors())).toEqual([
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ "line 1:4: mismatched input '' expecting 'where'",
+ ]);
+ });
+ });
+});
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts
new file mode 100644
index 0000000000000..63a812cad759a
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { get, has } from 'lodash';
+
+const PARSING_ERROR_TYPE = 'parsing_exception';
+const VERIFICATION_ERROR_TYPE = 'verification_exception';
+const MAPPING_ERROR_TYPE = 'mapping_exception';
+
+interface ErrorCause {
+ type: string;
+ reason: string;
+}
+
+export interface ErrorResponse {
+ error: ErrorCause & { root_cause: ErrorCause[] };
+}
+
+const isValidationErrorType = (type: unknown): boolean =>
+ type === PARSING_ERROR_TYPE || type === VERIFICATION_ERROR_TYPE || type === MAPPING_ERROR_TYPE;
+
+export const isErrorResponse = (response: unknown): response is ErrorResponse =>
+ has(response, 'error.type');
+
+export const isValidationErrorResponse = (response: unknown): response is ErrorResponse =>
+ isErrorResponse(response) && isValidationErrorType(get(response, 'error.type'));
+
+export const getValidationErrors = (response: ErrorResponse): string[] =>
+ response.error.root_cause
+ .filter((cause) => isValidationErrorType(cause.type))
+ .map((cause) => cause.reason);
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts
new file mode 100644
index 0000000000000..6c315f929b9bb
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './helpers';
diff --git a/x-pack/plugins/timelines/common/search_strategy/index.ts b/x-pack/plugins/timelines/common/search_strategy/index.ts
new file mode 100644
index 0000000000000..155306327ee0c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './common';
+export * from './timeline';
+export * from './index_fields';
+export * from './eql';
diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts
new file mode 100644
index 0000000000000..76ab48a8243db
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IIndexPattern } from 'src/plugins/data/public';
+import {
+ IEsSearchRequest,
+ IEsSearchResponse,
+ IFieldSubType,
+} from '../../../../../../src/plugins/data/common';
+import { DocValueFields, Maybe } from '../common';
+
+export type BeatFieldsFactoryQueryType = 'beatFields';
+
+interface FieldInfo {
+ category: string;
+ description?: string;
+ example?: string | number;
+ format?: string;
+ name: string;
+ type?: string;
+}
+
+export interface IndexField {
+ /** Where the field belong */
+ category: string;
+ /** Example of field's value */
+ example?: Maybe;
+ /** whether the field's belong to an alias index */
+ indexes: Array>;
+ /** The name of the field */
+ name: string;
+ /** The type of the field's values as recognized by Kibana */
+ type: string;
+ /** Whether the field's values can be efficiently searched for */
+ searchable: boolean;
+ /** Whether the field's values can be aggregated */
+ aggregatable: boolean;
+ /** Description of the field */
+ description?: Maybe;
+ format?: Maybe;
+ /** the elastic type as mapped in the index */
+ esTypes?: string[];
+ subType?: IFieldSubType;
+ readFromDocValues: boolean;
+}
+
+export type BeatFields = Record;
+
+export interface IndexFieldsStrategyRequest extends IEsSearchRequest {
+ indices: string[];
+ onlyCheckIfIndicesExist: boolean;
+}
+
+export interface IndexFieldsStrategyResponse extends IEsSearchResponse {
+ indexFields: IndexField[];
+ indicesExist: string[];
+}
+
+export interface BrowserField {
+ aggregatable: boolean;
+ category: string;
+ description: string | null;
+ example: string | number | null;
+ fields: Readonly>>;
+ format: string;
+ indexes: string[];
+ name: string;
+ searchable: boolean;
+ type: string;
+ subType?: {
+ [key: string]: unknown;
+ nested?: {
+ path: string;
+ };
+ };
+}
+
+export type BrowserFields = Readonly>>;
+
+export const EMPTY_BROWSER_FIELDS = {};
+export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = [];
+export const EMPTY_INDEX_PATTERN: IIndexPattern = {
+ fields: [],
+ title: '',
+};
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts
new file mode 100644
index 0000000000000..94f7bc617e2f2
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import type { Ecs } from '../../../../ecs';
+import type { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
+import type { TimelineRequestOptionsPaginated } from '../..';
+
+export interface TimelineEdges {
+ node: TimelineItem;
+ cursor: CursorType;
+}
+
+export interface TimelineItem {
+ _id: string;
+ _index?: Maybe;
+ data: TimelineNonEcsData[];
+ ecs: Ecs;
+}
+
+export interface TimelineNonEcsData {
+ field: string;
+ value?: Maybe;
+}
+
+export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse {
+ edges: TimelineEdges[];
+ totalCount: number;
+ pageInfo: Pick;
+ inspect?: Maybe;
+}
+
+export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated {
+ fields: string[] | Array<{ field: string; include_unmapped: boolean }>;
+ fieldRequested: string[];
+ language: 'eql' | 'kuery' | 'lucene';
+ excludeEcsData?: boolean;
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts
new file mode 100644
index 0000000000000..4a5bd2c99a0eb
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Ecs } from '../../../../ecs';
+import { CursorType, Maybe } from '../../../common';
+
+export interface TimelineEdges {
+ node: TimelineItem;
+ cursor: CursorType;
+}
+
+export interface TimelineItem {
+ _id: string;
+ _index?: Maybe;
+ data: TimelineNonEcsData[];
+ ecs: Ecs;
+}
+
+export interface TimelineNonEcsData {
+ field: string;
+ value?: Maybe;
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts
new file mode 100644
index 0000000000000..1f9820f8e5c2b
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import { Inspect, Maybe } from '../../../common';
+import { TimelineRequestOptionsPaginated } from '../..';
+
+export interface TimelineEventsDetailsItem {
+ ariaRowindex?: Maybe;
+ category?: string;
+ field: string;
+ values?: Maybe;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ originalValue?: Maybe;
+ isObjectArray: boolean;
+}
+
+export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse {
+ data?: Maybe;
+ inspect?: Maybe;
+}
+
+export interface TimelineEventsDetailsRequestOptions
+ extends Partial {
+ indexName: string;
+ eventId: string;
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts
new file mode 100644
index 0000000000000..1e5164684bf6e
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiComboBoxOptionOption } from '@elastic/eui';
+import {
+ EqlSearchStrategyRequest,
+ EqlSearchStrategyResponse,
+} from '../../../../../../../../src/plugins/data/common';
+import { EqlSearchResponse, Inspect, Maybe, PaginationInputPaginated } from '../../..';
+import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..';
+
+export interface TimelineEqlRequestOptions
+ extends EqlSearchStrategyRequest,
+ Omit {
+ eventCategoryField?: string;
+ tiebreakerField?: string;
+ timestampField?: string;
+ size?: number;
+}
+
+export interface TimelineEqlResponse extends EqlSearchStrategyResponse> {
+ edges: TimelineEdges[];
+ totalCount: number;
+ pageInfo: Pick;
+ inspect: Maybe;
+}
+
+export interface EqlOptionsData {
+ keywordFields: EuiComboBoxOptionOption[];
+ dateFields: EuiComboBoxOptionOption[];
+ nonDateFields: EuiComboBoxOptionOption[];
+}
+
+export interface EqlOptionsSelected {
+ eventCategoryField?: string;
+ tiebreakerField?: string;
+ timestampField?: string;
+ query?: string;
+ size?: number;
+}
+
+export type FieldsEqlOptions = keyof EqlOptionsSelected;
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts
new file mode 100644
index 0000000000000..c4d6f70a27587
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './all';
+export * from './details';
+export * from './last_event_time';
+export * from './eql';
+
+export enum TimelineEventsQueries {
+ all = 'eventsAll',
+ details = 'eventsDetails',
+ kpi = 'eventsKpi',
+ lastEventTime = 'eventsLastEventTime',
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts
new file mode 100644
index 0000000000000..f29dc4a3c7450
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import { Inspect, Maybe } from '../../../common';
+import { TimelineRequestBasicOptions } from '../..';
+
+export enum LastEventIndexKey {
+ hostDetails = 'hostDetails',
+ hosts = 'hosts',
+ ipDetails = 'ipDetails',
+ network = 'network',
+}
+
+export interface LastTimeDetails {
+ hostName?: Maybe;
+ ip?: Maybe;
+}
+
+export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchResponse {
+ lastSeen: Maybe;
+ inspect?: Maybe;
+}
+
+export interface TimelineKpiStrategyResponse extends IEsSearchResponse {
+ destinationIpCount: number;
+ inspect?: Maybe;
+ hostCount: number;
+ processCount: number;
+ sourceIpCount: number;
+ userCount: number;
+}
+
+export interface TimelineEventsLastEventTimeRequestOptions
+ extends Omit {
+ indexKey: LastEventIndexKey;
+ details: LastTimeDetails;
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts
new file mode 100644
index 0000000000000..7064ef033fc5a
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts
@@ -0,0 +1,197 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IEsSearchRequest } from '../../../../../../src/plugins/data/common';
+import { ESQuery } from '../../typed_json';
+import {
+ TimelineEventsQueries,
+ TimelineEventsAllRequestOptions,
+ TimelineEventsAllStrategyResponse,
+ TimelineEventsDetailsRequestOptions,
+ TimelineEventsDetailsStrategyResponse,
+ TimelineEventsLastEventTimeRequestOptions,
+ TimelineEventsLastEventTimeStrategyResponse,
+ TimelineKpiStrategyResponse,
+} from './events';
+import {
+ DocValueFields,
+ PaginationInputPaginated,
+ TimerangeInput,
+ SortField,
+ Maybe,
+} from '../common';
+import {
+ DataProviderType,
+ TimelineType,
+ TimelineStatus,
+ RowRendererId,
+} from '../../types/timeline';
+
+export * from './events';
+
+export type TimelineFactoryQueryTypes = TimelineEventsQueries;
+
+export interface TimelineRequestBasicOptions extends IEsSearchRequest {
+ timerange: TimerangeInput;
+ filterQuery: ESQuery | string | undefined;
+ defaultIndex: string[];
+ docValueFields?: DocValueFields[];
+ factoryQueryType?: TimelineFactoryQueryTypes;
+}
+
+export interface TimelineRequestSortField extends SortField {
+ type: string;
+}
+
+export interface TimelineRequestOptionsPaginated
+ extends TimelineRequestBasicOptions {
+ pagination: Pick;
+ sort: Array>;
+}
+
+export type TimelineStrategyResponseType<
+ T extends TimelineFactoryQueryTypes
+> = T extends TimelineEventsQueries.all
+ ? TimelineEventsAllStrategyResponse
+ : T extends TimelineEventsQueries.details
+ ? TimelineEventsDetailsStrategyResponse
+ : T extends TimelineEventsQueries.kpi
+ ? TimelineKpiStrategyResponse
+ : T extends TimelineEventsQueries.lastEventTime
+ ? TimelineEventsLastEventTimeStrategyResponse
+ : never;
+
+export type TimelineStrategyRequestType<
+ T extends TimelineFactoryQueryTypes
+> = T extends TimelineEventsQueries.all
+ ? TimelineEventsAllRequestOptions
+ : T extends TimelineEventsQueries.details
+ ? TimelineEventsDetailsRequestOptions
+ : T extends TimelineEventsQueries.kpi
+ ? TimelineRequestBasicOptions
+ : T extends TimelineEventsQueries.lastEventTime
+ ? TimelineEventsLastEventTimeRequestOptions
+ : never;
+
+export interface ColumnHeaderInput {
+ aggregatable?: Maybe;
+ category?: Maybe;
+ columnHeaderType?: Maybe;
+ description?: Maybe