diff --git a/.eslintrc.js b/.eslintrc.js
index 40dd6a55a2a3f6..c64f03a8398e54 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 267ab3891d7000..5bd3a7587dde9e 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 d7a368034ef07f..e7e25c7d3bba6d 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 48d0d40d0abb06..5d7ba22841aa16 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
@@ -104,6 +105,7 @@ yarn kbn watch-bazel
- @kbn/storybook
- @kbn/telemetry-utils
- @kbn/tinymath
+- @kbn/ui-framework
- @kbn/ui-shared-deps
- @kbn/utility-types
- @kbn/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 d3d76079cdc2a1..b10ad949c49443 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 34279cef198bfb..c020f57faa8825 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 3db8bbadfbd6bf..4d094ecde7a96a 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 6fc01212a2e41a..463c3fe81b7029 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 262b0997cb9050..43489b8d2e8a27 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 1805f389d4e7f3..7eaa9c51f5c82c 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/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 00000000000000..d649212ae05477
--- /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 1d3e0c08dfc18d..c7046902dac72b 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.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md
index b1745b298e27e8..9816b884c46144 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 00000000000000..e48a1c98f85785
--- /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 00000000000000..707739f845cd14
--- /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/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc
index b7b0c749dfe149..08655508b3cbab 100644
--- a/docs/user/alerting/alerting-troubleshooting.asciidoc
+++ b/docs/user/alerting/alerting-troubleshooting.asciidoc
@@ -12,6 +12,32 @@ If your problem isn’t described here, please review open issues in the followi
Have a question? Contact us in the https://discuss.elastic.co/[discuss forum].
+[float]
+[[rule-cannot-decrypt-api-key]]
+=== Rule cannot decrypt apiKey
+
+*Problem*:
+
+The rule fails to execute and has an `Unable to decrypt attribute "apiKey"` error.
+
+*Solution*:
+
+This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used during rule execution. Depending on the scenario, there are different ways to solve this problem:
+
+[cols="2*<"]
+|===
+
+| If the value in `xpack.encryptedSavedObjects.encryptionKey` was manually changed, and the previous encryption key is still known.
+| Ensure any previous encryption key is included in the keys used for <>.
+
+| If another {kib} instance with a different encryption key connects to the cluster.
+| The other {kib} instance might be trying to run the rule using a different encryption key than what the rule was created with. Ensure the encryption keys among all the {kib} instances are the same, and setting <> for previously used encryption keys.
+
+| If other scenarios don't apply.
+| Generate a new API key for the rule by disabling then enabling the rule.
+
+|===
+
[float]
[[rules-small-check-interval-run-late]]
=== Rules with small check intervals run late
@@ -29,7 +55,6 @@ Either tweak the <> or increa
For more details, see <>.
-
[float]
[[scheduled-rules-run-late]]
=== Rules run late
diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc
index cb5c484def3b9d..17bfc19c2e0c9d 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 4ecfcc92501228..2071f17ecff3d5 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 873dffeed38f8a..26465133569cd9 100644
--- a/package.json
+++ b/package.json
@@ -149,12 +149,13 @@
"@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",
"@kbn/std": "link:bazel-bin/packages/kbn-std",
"@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath",
- "@kbn/ui-framework": "link:packages/kbn-ui-framework",
+ "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework",
"@kbn/ui-shared-deps": "link:bazel-bin/packages/kbn-ui-shared-deps",
"@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types",
"@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils",
@@ -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",
@@ -446,8 +449,6 @@
"@bazel/typescript": "^3.5.1",
"@cypress/snapshot": "^2.1.7",
"@cypress/webpack-preprocessor": "^5.6.0",
- "@elastic/apm-rum": "^5.6.1",
- "@elastic/apm-rum-react": "^1.2.5",
"@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana",
"@elastic/eslint-plugin-eui": "0.0.2",
"@elastic/github-checks-reporter": "0.0.20b3",
@@ -464,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",
@@ -513,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 70a3d1eacc7c58..d9e2f0e1f99854 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",
@@ -48,6 +50,7 @@ filegroup(
"//packages/kbn-storybook:build",
"//packages/kbn-telemetry-tools:build",
"//packages/kbn-tinymath:build",
+ "//packages/kbn-ui-framework:build",
"//packages/kbn-ui-shared-deps:build",
"//packages/kbn-utility-types:build",
"//packages/kbn-utils:build",
diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json
index dd491de55c075d..cf6fcfd88a26da 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 00000000000000..3809c2b33d5009
--- /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 f9127e4629f43e..c6960621359c78 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 a6c8284ad15f64..d23512f7c418d6 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 c175979f0e820e..1f1e33d3dda7c8 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 92875d3f69e465..d9e1bee22557bf 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 f2d508cf14a55e..76beaf7689fd41 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 2d642d9ede13bc..36a37075191a37 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-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts
index f75f0dcebf4f67..1909bcb1bcc2e6 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 a9a93aa8df49a6..0bd4c6c705668d 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 00000000000000..934a9cbff56a64
--- /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 00000000000000..7e55073228fcab
--- /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 327a29dc1b987a..bfaad52ee81472 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 c9dd6ccae484c2..238ae5541343cf 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 00000000000000..5cf1081bdd32e9
--- /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 00000000000000..a49669c81689a2
--- /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 00000000000000..b4a118df51af51
--- /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 00000000000000..21e7d2d71b61a1
--- /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 00000000000000..68d3a8c71e7cac
--- /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 00000000000000..c29ddd45f084d8
--- /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 00000000000000..c03c0093d98392
--- /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 00000000000000..0c2e9a7dbea8bf
--- /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 00000000000000..dc1b63dfc33b03
--- /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 7dc257ebb3feff..167fc9dd17a2ac 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 00000000000000..34e448419693bb
--- /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 00000000000000..91b2e88d973589
--- /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 00000000000000..39629a990c539c
--- /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 00000000000000..a5183ba4fd4576
--- /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 00000000000000..8cda578edede4c
--- /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 225f93d4878238..5baff607704c78 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(?![\\/\\\\](monaco-editor|react-monaco-editor))[/\\\\].+\\.js$',
+ '[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor|d3-interpolate|d3-color))[/\\\\].+\\.js$',
'packages/kbn-pm/dist/index.js',
],
diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json
index 275d9fac73c58d..aaff513f1591f2 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 d112e4d4fcb393..7dda4cceec65cd 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-ui-framework/BUILD.bazel b/packages/kbn-ui-framework/BUILD.bazel
new file mode 100644
index 00000000000000..f8cf5035bdc5f3
--- /dev/null
+++ b/packages/kbn-ui-framework/BUILD.bazel
@@ -0,0 +1,47 @@
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-ui-framework"
+PKG_REQUIRE_NAME = "@kbn/ui-framework"
+
+SOURCE_FILES = glob([
+ "dist/**/*",
+])
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "package.json",
+ "README.md",
+]
+
+DEPS = []
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES + [
+ ":srcs",
+ ],
+ deps = DEPS,
+ 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/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts
index 0264c8a1acf754..92f5a854f6b00f 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 5ed447edde75a0..f1381c52ce7793 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/core_system.test.ts b/src/core/public/core_system.test.ts
index 1c4e78f0a5c2ef..8ead0f50785bdc 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 f0ea1e62fc33f8..9a28bf45df9273 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 95091a761639b6..502b22a6f8e89c 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/public.api.md b/src/core/public/public.api.md
index 6cc2b3f321fb7c..ca95b253f9cdbb 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 4bd6afe90d3429..92ba28ff70887e 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/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 22c40a547f419a..4456784fdbc0b4 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 1577f773434b9d..c9fa50da55df10 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 af682cfb81296e..1423050145695f 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 9e7721fde90e7d..fcecf39f7e53a9 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/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 00000000000000..9221af3142e613
--- /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/typescript/projects.ts b/src/dev/typescript/projects.ts
index 050743114f657d..f372cf052d3683 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,
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 4d8ee0f8891732..91379ea054de3b 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 d1890ec97df4e1..c5cf3f9f09e6c7 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/public.api.md b/src/plugins/data/public/public.api.md
index 4d9c69b137a3e0..7a5f323e51459a 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -1351,6 +1351,7 @@ export interface IKibanaSearchRequest {
export interface IKibanaSearchResponse {
id?: string;
isPartial?: boolean;
+ isRestored?: boolean;
isRunning?: boolean;
loaded?: number;
rawResponse: RawResponse;
diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts
index 82c9e04b797983..fcdea8dec1c2eb 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 00000000000000..c5c5c37f31cf87
--- /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 fe66d4b6e99370..155638250a2a4c 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 57b156a9b3c00a..e0e1df65101c7d 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/server/index.ts b/src/plugins/data/server/index.ts
index 0764f4f441e425..dd60951e6d2285 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 00000000000000..b291df1cee5ba7
--- /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 812f3171aef99f..b9affe96ea2ddd 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 52ee8e60a5b26a..314cb2c3acbf87 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 a651d7b3bf105e..00dffefa5e3a6e 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 c2b533bc42dc6f..768c44d3e3e950 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -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/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx
index 2fd394d98281b7..57a9d518f838ef 100644
--- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx
@@ -18,7 +18,6 @@ import { createSearchSourceMock } from '../../../../../../../data/common/search/
import { IndexPattern, IndexPatternAttributes } from '../../../../../../../data/common';
import { SavedObject } from '../../../../../../../../core/types';
import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
-import { DiscoverSearchSessionManager } from '../../services/discover_search_session';
import { GetStateReturn } from '../../services/discover_state';
import { DiscoverLayoutProps } from './types';
import { SavedSearchDataSubject } from '../../services/use_saved_search';
@@ -50,11 +49,12 @@ function getProps(indexPattern: IndexPattern): DiscoverLayoutProps {
indexPattern,
indexPatternList,
navigateTo: jest.fn(),
+ onChangeIndexPattern: jest.fn(),
+ onUpdateQuery: jest.fn(),
resetQuery: jest.fn(),
savedSearch: savedSearchMock,
savedSearchData$: savedSearch$,
savedSearchRefetch$: new Subject(),
- searchSessionManager: {} as DiscoverSearchSessionManager,
searchSource: searchSourceMock,
services,
state: { columns: [] },
diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx
index 0430614d413b6b..a10674323e5cbf 100644
--- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx
@@ -36,10 +36,8 @@ import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
DOC_TABLE_LEGACY,
- MODIFY_COLUMNS_ON_SWITCH,
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
- SORT_DEFAULT_ORDER_SETTING,
} from '../../../../../../common';
import { popularizeField } from '../../../../helpers/popularize_field';
import { DocViewFilterFn } from '../../../../doc_views/doc_views_types';
@@ -52,7 +50,6 @@ import { InspectorSession } from '../../../../../../../inspector/public';
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
import { SavedSearchDataMessage } from '../../services/use_saved_search';
import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns';
-import { getSwitchIndexPatternAppState } from '../../utils/get_switch_index_pattern_app_state';
import { FetchStatus } from '../../../../types';
const DocTableLegacyMemoized = React.memo(DocTableLegacy);
@@ -72,26 +69,20 @@ export function DiscoverLayout({
indexPattern,
indexPatternList,
navigateTo,
+ onChangeIndexPattern,
+ onUpdateQuery,
savedSearchRefetch$,
resetQuery,
savedSearchData$,
savedSearch,
- searchSessionManager,
searchSource,
services,
state,
stateContainer,
}: DiscoverLayoutProps) {
- const {
- trackUiMetric,
- capabilities,
- indexPatterns,
- data,
- uiSettings: config,
- filterManager,
- } = services;
+ const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services;
- const sampleSize = useMemo(() => config.get(SAMPLE_SIZE_SETTING), [config]);
+ const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]);
const [expandedDoc, setExpandedDoc] = useState(undefined);
const [inspectorSession, setInspectorSession] = useState(undefined);
const scrollableDesktop = useRef(null);
@@ -121,42 +112,21 @@ export function DiscoverLayout({
};
}, [savedSearchData$, fetchState]);
- const isMobile = () => {
- // collapse icon isn't displayed in mobile view, use it to detect which view is displayed
- return collapseIcon && !collapseIcon.current;
- };
+ // collapse icon isn't displayed in mobile view, use it to detect which view is displayed
+ const isMobile = () => collapseIcon && !collapseIcon.current;
const timeField = useMemo(() => {
return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined;
}, [indexPattern]);
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
- const isLegacy = useMemo(() => services.uiSettings.get(DOC_TABLE_LEGACY), [services]);
- const useNewFieldsApi = useMemo(() => !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [
- services,
- ]);
-
- const unmappedFieldsConfig = useMemo(
- () => ({
- showUnmappedFields: useNewFieldsApi,
- }),
- [useNewFieldsApi]
- );
+ const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
+ const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const resultState = useMemo(() => getResultState(fetchStatus, rows!), [fetchStatus, rows]);
- const updateQuery = useCallback(
- (_payload, isUpdate?: boolean) => {
- if (isUpdate === false) {
- searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
- savedSearchRefetch$.next();
- }
- },
- [savedSearchRefetch$, searchSessionManager]
- );
-
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({
capabilities,
- config,
+ config: uiSettings,
indexPattern,
indexPatterns,
setAppState: stateContainer.setAppState,
@@ -243,42 +213,8 @@ export function DiscoverLayout({
const contentCentered = resultState === 'uninitialized';
const showTimeCol = useMemo(
- () => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
- [config, indexPattern.timeFieldName]
- );
-
- const onChangeIndexPattern = useCallback(
- async (id: string) => {
- const nextIndexPattern = await indexPatterns.get(id);
- if (nextIndexPattern && indexPattern) {
- /**
- * Without resetting the fetch state, e.g. a time column would be displayed when switching
- * from a index pattern without to a index pattern with time filter for a brief moment
- * That's because appState is updated before savedSearchData$
- * The following line of code catches this, but should be improved
- */
- savedSearchData$.next({ rows: [], state: FetchStatus.LOADING, fieldCounts: {} });
-
- const nextAppState = getSwitchIndexPatternAppState(
- indexPattern,
- nextIndexPattern,
- state.columns || [],
- (state.sort || []) as SortPairArr[],
- config.get(MODIFY_COLUMNS_ON_SWITCH),
- config.get(SORT_DEFAULT_ORDER_SETTING)
- );
- stateContainer.setAppState(nextAppState);
- }
- },
- [
- config,
- indexPattern,
- indexPatterns,
- savedSearchData$,
- state.columns,
- state.sort,
- stateContainer,
- ]
+ () => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
+ [uiSettings, indexPattern.timeFieldName]
);
return (
@@ -294,7 +230,7 @@ export function DiscoverLayout({
searchSource={searchSource}
services={services}
stateContainer={stateContainer}
- updateQuery={updateQuery}
+ updateQuery={onUpdateQuery}
/>
@@ -316,7 +252,6 @@ export function DiscoverLayout({
state={state}
isClosed={isSidebarClosed}
trackUiMetric={trackUiMetric}
- unmappedFieldsConfig={unmappedFieldsConfig}
useNewFieldsApi={useNewFieldsApi}
onEditRuntimeField={onEditRuntimeField}
/>
@@ -373,7 +308,7 @@ export function DiscoverLayout({
>
>;
- resetQuery: () => void;
navigateTo: (url: string) => void;
+ onChangeIndexPattern: (id: string) => void;
+ onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
+ resetQuery: () => void;
savedSearch: SavedSearch;
savedSearchData$: SavedSearchDataSubject;
savedSearchRefetch$: SavedSearchRefetchSubject;
- searchSessionManager: DiscoverSearchSessionManager;
searchSource: ISearchSource;
services: DiscoverServices;
state: AppState;
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 00000000000000..8c32942740a768
--- /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 d5076e4daa9904..5f2f35e2419dd7 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/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
index 7fbbf6fd3ffdc7..7f8866a2ee369d 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
@@ -82,7 +82,6 @@ export function DiscoverSidebar({
trackUiMetric,
useNewFieldsApi = false,
useFlyout = false,
- unmappedFieldsConfig,
onEditRuntimeField,
onChangeIndexPattern,
setFieldEditorRef,
@@ -129,25 +128,8 @@ export function DiscoverSidebar({
popular: popularFields,
unpopular: unpopularFields,
} = useMemo(
- () =>
- groupFields(
- fields,
- columns,
- popularLimit,
- fieldCounts,
- fieldFilter,
- useNewFieldsApi,
- !!unmappedFieldsConfig?.showUnmappedFields
- ),
- [
- fields,
- columns,
- popularLimit,
- fieldCounts,
- fieldFilter,
- useNewFieldsApi,
- unmappedFieldsConfig?.showUnmappedFields,
- ]
+ () => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi),
+ [fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi]
);
const paginate = useCallback(() => {
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
index 2ad75806173eb0..6973221fd36248 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
@@ -25,7 +25,6 @@ import {
} from './discover_sidebar_responsive';
import { DiscoverServices } from '../../../../../build_services';
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
-import { DiscoverSidebar } from './discover_sidebar';
const mockServices = ({
history: () => ({
@@ -132,14 +131,4 @@ describe('discover responsive sidebar', function () {
findTestSubject(comp, 'plus-extension-gif').simulate('click');
expect(props.onAddFilter).toHaveBeenCalled();
});
- it('renders sidebar with unmapped fields config', function () {
- const unmappedFieldsConfig = {
- showUnmappedFields: false,
- };
- const componentProps = { ...props, unmappedFieldsConfig };
- const component = mountWithIntl();
- const discoverSidebar = component.find(DiscoverSidebar);
- expect(discoverSidebar).toHaveLength(1);
- expect(discoverSidebar.props().unmappedFieldsConfig).toEqual(unmappedFieldsConfig);
- });
});
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
index cc33601f77728f..003bb22599e480 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
@@ -105,15 +105,6 @@ export interface DiscoverSidebarResponsiveProps {
* Read from the Fields API
*/
useNewFieldsApi?: boolean;
- /**
- * an object containing properties for proper handling of unmapped fields
- */
- unmappedFieldsConfig?: {
- /**
- * determines whether to display unmapped fields
- */
- showUnmappedFields: boolean;
- };
/**
* callback to execute on edit runtime field
*/
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts
index 58697206356214..cd9f6b3cac4a51 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts
@@ -244,8 +244,7 @@ describe('group_fields', function () {
5,
fieldCounts,
fieldFilterState,
- true,
- false
+ true
);
expect(actual.unpopular).toEqual([]);
});
@@ -270,8 +269,7 @@ describe('group_fields', function () {
5,
fieldCounts,
fieldFilterState,
- false,
- undefined
+ false
);
expect(actual.unpopular.map((field) => field.name)).toEqual(['unknown_field']);
});
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx
index dc6cbcedc80864..2007d32fe84bee 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx
@@ -24,9 +24,9 @@ export function groupFields(
popularLimit: number,
fieldCounts: Record,
fieldFilterState: FieldFilterState,
- useNewFieldsApi: boolean,
- showUnmappedFields = true
+ useNewFieldsApi: boolean
): GroupedFields {
+ const showUnmappedFields = useNewFieldsApi;
const result: GroupedFields = {
selected: [],
popular: [],
diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx
index 5cc7147b49ff98..07939fff6e7f48 100644
--- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx
+++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx
@@ -5,15 +5,12 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import React, { useMemo, useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect } from 'react';
import { History } from 'history';
import { DiscoverLayout } from './components/layout';
-import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
-import { useSavedSearch as useSavedSearchData } from './services/use_saved_search';
import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs';
import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util';
import { useDiscoverState } from './services/use_discover_state';
-import { useSearchSession } from './services/use_search_session';
import { useUrl } from './services/use_url';
import { IndexPattern, IndexPatternAttributes, SavedObject } from '../../../../../data/common';
import { DiscoverServices } from '../../../build_services';
@@ -55,18 +52,20 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
const { services, history, navigateTo, indexPatternList } = props.opts;
const { chrome, docLinks, uiSettings: config, data } = services;
- const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]);
-
/**
* State related logic
*/
const {
- stateContainer,
- state,
+ data$,
indexPattern,
- searchSource,
- savedSearch,
+ onChangeIndexPattern,
+ onUpdateQuery,
+ refetch$,
resetSavedSearch,
+ savedSearch,
+ searchSource,
+ state,
+ stateContainer,
} = useDiscoverState({
services,
history,
@@ -79,25 +78,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
*/
useUrl({ history, resetSavedSearch });
- /**
- * Search session logic
- */
- const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch });
-
- /**
- * Data fetching logic
- */
- const { data$, refetch$ } = useSavedSearchData({
- indexPattern,
- savedSearch,
- searchSessionManager,
- searchSource,
- services,
- state,
- stateContainer,
- useNewFieldsApi,
- });
-
/**
* SavedSearch depended initializing
*/
@@ -115,11 +95,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
*/
useEffect(() => {
addHelpMenuToAppChrome(chrome, docLinks);
- stateContainer.replaceUrlAppState({}).then(() => {
- stateContainer.startSync();
- });
-
- return () => stateContainer.stopSync();
}, [stateContainer, chrome, docLinks]);
const resetQuery = useCallback(() => {
@@ -130,12 +105,13 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
;
+ /**
+ * Function starting state sync when Discover main is loaded
+ */
+ initializeAndSync: (
+ indexPattern: IndexPattern,
+ filterManager: FilterManager,
+ data: DataPublicPluginStart
+ ) => () => void;
/**
* Start sync between state and URL
*/
@@ -204,16 +216,18 @@ export function getState({
stateStorage,
});
+ const replaceUrlAppState = async (newPartial: AppState = {}) => {
+ const state = { ...appStateContainer.getState(), ...newPartial };
+ await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true });
+ };
+
return {
kbnUrlStateStorage: stateStorage,
appStateContainer: appStateContainerModified,
startSync: start,
stopSync: stop,
setAppState: (newPartial: AppState) => setState(appStateContainerModified, newPartial),
- replaceUrlAppState: async (newPartial: AppState = {}) => {
- const state = { ...appStateContainer.getState(), ...newPartial };
- await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true });
- },
+ replaceUrlAppState,
resetInitialAppState: () => {
initialAppState = appStateContainer.getState();
},
@@ -224,6 +238,50 @@ export function getState({
getPreviousAppState: () => previousAppState,
flushToUrl: () => stateStorage.kbnUrlControls.flush(),
isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()),
+ initializeAndSync: (
+ indexPattern: IndexPattern,
+ filterManager: FilterManager,
+ data: DataPublicPluginStart
+ ) => {
+ if (appStateContainer.getState().index !== indexPattern.id) {
+ // used index pattern is different than the given by url/state which is invalid
+ setState(appStateContainerModified, { index: indexPattern.id });
+ }
+ // sync initial app filters from state to filterManager
+ const filters = appStateContainer.getState().filters;
+ if (filters) {
+ filterManager.setAppFilters(cloneDeep(filters));
+ }
+ const query = appStateContainer.getState().query;
+ if (query) {
+ data.query.queryString.setQuery(query);
+ }
+
+ const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
+ data.query,
+ appStateContainer,
+ {
+ filters: esFilters.FilterStateStore.APP_STATE,
+ query: true,
+ }
+ );
+
+ // syncs `_g` portion of url with query services
+ const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl(
+ data.query,
+ stateStorage
+ );
+
+ replaceUrlAppState({}).then(() => {
+ start();
+ });
+
+ return () => {
+ stopSyncingQueryAppStateWithStateContainer();
+ stopSyncingGlobalStateWithUrl();
+ stop();
+ };
+ },
};
}
diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts
index 051a2d2dcd9cc7..4c3d819f063a0d 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts
@@ -62,10 +62,6 @@ describe('test useDiscoverState', () => {
});
});
- await act(async () => {
- result.current.stateContainer.startSync();
- });
-
const initialColumns = result.current.state.columns;
await act(async () => {
result.current.setState({ columns: ['123'] });
diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
index a3546d54cd4932..3c736f09a82967 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
@@ -6,19 +6,25 @@
* Side Public License, v 1.
*/
import { useMemo, useEffect, useState, useCallback } from 'react';
-import { cloneDeep } from 'lodash';
+import { isEqual } from 'lodash';
import { History } from 'history';
import { getState } from './discover_state';
import { getStateDefaults } from '../utils/get_state_defaults';
-import {
- esFilters,
- connectToQueryState,
- syncQueryStateWithUrl,
- IndexPattern,
-} from '../../../../../../data/public';
+import { IndexPattern } from '../../../../../../data/public';
import { DiscoverServices } from '../../../../build_services';
import { SavedSearch } from '../../../../saved_searches';
import { loadIndexPattern } from '../utils/resolve_index_pattern';
+import { useSavedSearch as useSavedSearchData } from './use_saved_search';
+import {
+ MODIFY_COLUMNS_ON_SWITCH,
+ SEARCH_FIELDS_FROM_SOURCE,
+ SEARCH_ON_PAGE_LOAD_SETTING,
+ SORT_DEFAULT_ORDER_SETTING,
+} from '../../../../../common';
+import { useSearchSession } from './use_search_session';
+import { FetchStatus } from '../../../types';
+import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state';
+import { SortPairArr } from '../../../angular/doc_table/lib/get_sort';
export function useDiscoverState({
services,
@@ -31,9 +37,11 @@ export function useDiscoverState({
history: History;
initialIndexPattern: IndexPattern;
}) {
- const { uiSettings: config, data, filterManager } = services;
+ const { uiSettings: config, data, filterManager, indexPatterns } = services;
const [indexPattern, setIndexPattern] = useState(initialIndexPattern);
const [savedSearch, setSavedSearch] = useState(initialSavedSearch);
+ const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]);
+ const timefilter = data.query.timefilter.timefilter;
const searchSource = useMemo(() => {
savedSearch.searchSource.setField('index', indexPattern);
@@ -57,73 +65,80 @@ export function useDiscoverState({
[config, data, history, savedSearch, services.core.notifications.toasts]
);
- const { appStateContainer, getPreviousAppState } = stateContainer;
+ const { appStateContainer } = stateContainer;
const [state, setState] = useState(appStateContainer.getState());
- useEffect(() => {
- if (stateContainer.appStateContainer.getState().index !== indexPattern.id) {
- // used index pattern is different than the given by url/state which is invalid
- stateContainer.setAppState({ index: indexPattern.id });
- }
- // sync initial app filters from state to filterManager
- const filters = appStateContainer.getState().filters;
- if (filters) {
- filterManager.setAppFilters(cloneDeep(filters));
- }
- const query = appStateContainer.getState().query;
- if (query) {
- data.query.queryString.setQuery(query);
- }
+ /**
+ * Search session logic
+ */
+ const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch });
- const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
- data.query,
- appStateContainer,
- {
- filters: esFilters.FilterStateStore.APP_STATE,
- query: true,
- }
- );
+ const initialFetchStatus: FetchStatus = useMemo(() => {
+ // A saved search is created on every page load, so we check the ID to see if we're loading a
+ // previously saved search or if it is just transient
+ const shouldSearchOnPageLoad =
+ config.get(SEARCH_ON_PAGE_LOAD_SETTING) ||
+ savedSearch.id !== undefined ||
+ timefilter.getRefreshInterval().pause === false ||
+ searchSessionManager.hasSearchSessionIdInURL();
+ return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED;
+ }, [config, savedSearch.id, searchSessionManager, timefilter]);
- // syncs `_g` portion of url with query services
- const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl(
- data.query,
- stateContainer.kbnUrlStateStorage
- );
+ /**
+ * Data fetching logic
+ */
+ const { data$, refetch$, reset } = useSavedSearchData({
+ indexPattern,
+ initialFetchStatus,
+ searchSessionManager,
+ searchSource,
+ services,
+ stateContainer,
+ useNewFieldsApi,
+ });
+
+ useEffect(() => {
+ const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data);
return () => {
- stopSyncingQueryAppStateWithStateContainer();
- stopSyncingGlobalStateWithUrl();
+ stopSync();
};
- }, [
- appStateContainer,
- config,
- data.query,
- data.search.session,
- getPreviousAppState,
- indexPattern.id,
- filterManager,
- services.indexPatterns,
- stateContainer,
- ]);
+ }, [stateContainer, filterManager, data, indexPattern]);
+ /**
+ * Track state changes that should trigger a fetch
+ */
useEffect(() => {
- const unsubscribe = stateContainer.appStateContainer.subscribe(async (nextState) => {
+ const unsubscribe = appStateContainer.subscribe(async (nextState) => {
+ const { hideChart, interval, sort, index } = state;
+ // chart was hidden, now it should be displayed, so data is needed
+ const chartDisplayChanged = nextState.hideChart !== hideChart && hideChart;
+ const chartIntervalChanged = nextState.interval !== interval;
+ const docTableSortChanged = !isEqual(nextState.sort, sort);
+ const indexPatternChanged = !isEqual(nextState.index, index);
// NOTE: this is also called when navigating from discover app to context app
- if (nextState.index && state.index !== nextState.index) {
- const nextIndexPattern = await loadIndexPattern(
- nextState.index,
- services.indexPatterns,
- config
- );
+ if (nextState.index && indexPatternChanged) {
+ /**
+ * Without resetting the fetch state, e.g. a time column would be displayed when switching
+ * from a index pattern without to a index pattern with time filter for a brief moment
+ * That's because appState is updated before savedSearchData$
+ * The following line of code catches this, but should be improved
+ */
+ reset();
+ const nextIndexPattern = await loadIndexPattern(nextState.index, indexPatterns, config);
if (nextIndexPattern) {
setIndexPattern(nextIndexPattern.loaded);
}
}
+
+ if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) {
+ refetch$.next();
+ }
setState(nextState);
});
return () => unsubscribe();
- }, [config, services.indexPatterns, state.index, stateContainer.appStateContainer, setState]);
+ }, [config, indexPatterns, appStateContainer, setState, state, refetch$, data$, reset]);
const resetSavedSearch = useCallback(
async (id?: string) => {
@@ -143,13 +158,62 @@ export function useDiscoverState({
[services, indexPattern, config, data, stateContainer, savedSearch.id]
);
+ /**
+ * Function triggered when user changes index pattern in the sidebar
+ */
+ const onChangeIndexPattern = useCallback(
+ async (id: string) => {
+ const nextIndexPattern = await indexPatterns.get(id);
+ if (nextIndexPattern && indexPattern) {
+ const nextAppState = getSwitchIndexPatternAppState(
+ indexPattern,
+ nextIndexPattern,
+ state.columns || [],
+ (state.sort || []) as SortPairArr[],
+ config.get(MODIFY_COLUMNS_ON_SWITCH),
+ config.get(SORT_DEFAULT_ORDER_SETTING)
+ );
+ stateContainer.setAppState(nextAppState);
+ }
+ },
+ [config, indexPattern, indexPatterns, state.columns, state.sort, stateContainer]
+ );
+ /**
+ * Function triggered when the user changes the query in the search bar
+ */
+ const onUpdateQuery = useCallback(
+ (_payload, isUpdate?: boolean) => {
+ if (isUpdate === false) {
+ searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
+ refetch$.next();
+ }
+ },
+ [refetch$, searchSessionManager]
+ );
+
+ /**
+ * Initial data fetching, also triggered when index pattern changes
+ */
+ useEffect(() => {
+ if (!indexPattern) {
+ return;
+ }
+ if (initialFetchStatus === FetchStatus.LOADING) {
+ refetch$.next();
+ }
+ }, [initialFetchStatus, refetch$, indexPattern, data$]);
+
return {
- state,
- setState,
- stateContainer,
+ data$,
indexPattern,
- searchSource,
- savedSearch,
+ refetch$,
resetSavedSearch,
+ onChangeIndexPattern,
+ onUpdateQuery,
+ savedSearch,
+ searchSource,
+ setState,
+ state,
+ stateContainer,
};
}
diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts
index 5976c8fea5ea4f..128c94f284f56c 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts
@@ -12,9 +12,10 @@ import { discoverServiceMock } from '../../../../__mocks__/services';
import { savedSearchMock } from '../../../../__mocks__/saved_search';
import { indexPatternMock } from '../../../../__mocks__/index_pattern';
import { useSavedSearch } from './use_saved_search';
-import { AppState, getState } from './discover_state';
+import { getState } from './discover_state';
import { uiSettingsMock } from '../../../../__mocks__/ui_settings';
import { useDiscoverState } from './use_discover_state';
+import { FetchStatus } from '../../../types';
describe('test useSavedSearch', () => {
test('useSavedSearch return is valid', async () => {
@@ -28,11 +29,10 @@ describe('test useSavedSearch', () => {
const { result } = renderHook(() => {
return useSavedSearch({
indexPattern: indexPatternMock,
- savedSearch: savedSearchMock,
+ initialFetchStatus: FetchStatus.LOADING,
searchSessionManager,
searchSource: savedSearchMock.searchSource.createCopy(),
services: discoverServiceMock,
- state: {} as AppState,
stateContainer,
useNewFieldsApi: true,
});
@@ -69,11 +69,10 @@ describe('test useSavedSearch', () => {
const { result, waitForValueToChange } = renderHook(() => {
return useSavedSearch({
indexPattern: indexPatternMock,
- savedSearch: savedSearchMock,
+ initialFetchStatus: FetchStatus.LOADING,
searchSessionManager,
searchSource: resultState.current.searchSource,
services: discoverServiceMock,
- state: {} as AppState,
stateContainer,
useNewFieldsApi: true,
});
@@ -88,4 +87,47 @@ describe('test useSavedSearch', () => {
expect(result.current.data$.value.hits).toBe(0);
expect(result.current.data$.value.rows).toEqual([]);
});
+
+ test('reset sets back to initial state', async () => {
+ const { history, searchSessionManager } = createSearchSessionMock();
+ const stateContainer = getState({
+ getStateDefaults: () => ({ index: 'the-index-pattern-id' }),
+ history,
+ uiSettings: uiSettingsMock,
+ });
+
+ discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => {
+ return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
+ });
+
+ const { result: resultState } = renderHook(() => {
+ return useDiscoverState({
+ services: discoverServiceMock,
+ history,
+ initialIndexPattern: indexPatternMock,
+ initialSavedSearch: savedSearchMock,
+ });
+ });
+
+ const { result, waitForValueToChange } = renderHook(() => {
+ return useSavedSearch({
+ indexPattern: indexPatternMock,
+ initialFetchStatus: FetchStatus.LOADING,
+ searchSessionManager,
+ searchSource: resultState.current.searchSource,
+ services: discoverServiceMock,
+ stateContainer,
+ useNewFieldsApi: true,
+ });
+ });
+
+ result.current.refetch$.next();
+
+ await waitForValueToChange(() => {
+ return result.current.data$.value.state === FetchStatus.COMPLETE;
+ });
+
+ result.current.reset();
+ expect(result.current.data$.value.state).toBe(FetchStatus.LOADING);
+ });
});
diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
index 2b0d9517248694..8c847b54078eb7 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
@@ -5,11 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import { useEffect, useRef, useCallback, useMemo } from 'react';
+import { useEffect, useRef, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { merge, Subject, BehaviorSubject } from 'rxjs';
import { debounceTime, tap, filter } from 'rxjs/operators';
-import { isEqual } from 'lodash';
import { DiscoverServices } from '../../../../build_services';
import { DiscoverSearchSessionManager } from './discover_search_session';
import {
@@ -18,13 +17,11 @@ import {
SearchSource,
tabifyAggResponse,
} from '../../../../../../data/common';
-import { SavedSearch } from '../../../../saved_searches';
-import { AppState, GetStateReturn } from './discover_state';
+import { GetStateReturn } from './discover_state';
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
import { RequestAdapter } from '../../../../../../inspector/public';
import { AutoRefreshDoneFn, search } from '../../../../../../data/public';
import { calcFieldCounts } from '../utils/calc_field_counts';
-import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../../common';
import { validateTimeRange } from '../utils/validate_time_range';
import { updateSearchSource } from '../utils/update_search_source';
import { SortOrder } from '../../../../saved_searches/types';
@@ -40,6 +37,7 @@ export type SavedSearchRefetchSubject = Subject;
export interface UseSavedSearch {
refetch$: SavedSearchRefetchSubject;
data$: SavedSearchDataSubject;
+ reset: () => void;
}
export type SavedSearchRefetchMsg = 'reset' | undefined;
@@ -59,48 +57,27 @@ export interface SavedSearchDataMessage {
/**
* This hook return 2 observables, refetch$ allows to trigger data fetching, data$ to subscribe
* to the data fetching
- * @param indexPattern
- * @param savedSearch
- * @param searchSessionManager
- * @param searchSource
- * @param services
- * @param state
- * @param stateContainer
- * @param useNewFieldsApi
*/
export const useSavedSearch = ({
indexPattern,
- savedSearch,
+ initialFetchStatus,
searchSessionManager,
searchSource,
services,
- state,
stateContainer,
useNewFieldsApi,
}: {
indexPattern: IndexPattern;
- savedSearch: SavedSearch;
+ initialFetchStatus: FetchStatus;
searchSessionManager: DiscoverSearchSessionManager;
searchSource: SearchSource;
services: DiscoverServices;
- state: AppState;
stateContainer: GetStateReturn;
useNewFieldsApi: boolean;
}): UseSavedSearch => {
- const { data, filterManager, uiSettings } = services;
+ const { data, filterManager } = services;
const timefilter = data.query.timefilter.timefilter;
- const initFetchState: FetchStatus = useMemo(() => {
- // A saved search is created on every page load, so we check the ID to see if we're loading a
- // previously saved search or if it is just transient
- const shouldSearchOnPageLoad =
- uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) ||
- savedSearch.id !== undefined ||
- timefilter.getRefreshInterval().pause === false ||
- searchSessionManager.hasSearchSessionIdInURL();
- return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED;
- }, [uiSettings, savedSearch.id, searchSessionManager, timefilter]);
-
/**
* The observable the UI (aka React component) subscribes to get notified about
* the changes in the data fetching process (high level: fetching started, data was received)
@@ -108,7 +85,7 @@ export const useSavedSearch = ({
const data$: SavedSearchDataSubject = useSingleton(
() =>
new BehaviorSubject({
- state: initFetchState,
+ state: initialFetchStatus,
})
);
/**
@@ -123,15 +100,14 @@ export const useSavedSearch = ({
*/
const refs = useRef<{
abortController?: AbortController;
- /**
- * used to compare a new state against an old one, to evaluate if data needs to be fetched
- */
- appState: AppState;
/**
* handler emitted by `timefilter.getAutoRefreshFetch$()`
* to notify when data completed loading and to start a new autorefresh loop
*/
autoRefreshDoneCb?: AutoRefreshDoneFn;
+ /**
+ * Number of fetches used for functional testing
+ */
fetchCounter: number;
/**
* needed to right auto refresh behavior, a new auto refresh shouldnt be triggered when
@@ -144,12 +120,34 @@ export const useSavedSearch = ({
*/
fieldCounts: Record;
}>({
- appState: state,
fetchCounter: 0,
fieldCounts: {},
- fetchStatus: initFetchState,
+ fetchStatus: initialFetchStatus,
});
+ /**
+ * Resets the fieldCounts cache and sends a reset message
+ * It is set to initial state (no documents, fetchCounter to 0)
+ * Needed when index pattern is switched or a new runtime field is added
+ */
+ const sendResetMsg = useCallback(
+ (fetchStatus?: FetchStatus) => {
+ refs.current.fieldCounts = {};
+ refs.current.fetchStatus = fetchStatus ?? initialFetchStatus;
+ data$.next({
+ state: initialFetchStatus,
+ fetchCounter: 0,
+ rows: [],
+ fieldCounts: {},
+ chartData: undefined,
+ bucketInterval: undefined,
+ });
+ },
+ [data$, initialFetchStatus]
+ );
+ /**
+ * Function to fetch data from ElasticSearch
+ */
const fetchAll = useCallback(
(reset = false) => {
if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) {
@@ -161,23 +159,18 @@ export const useSavedSearch = ({
refs.current.abortController = new AbortController();
const sessionId = searchSessionManager.getNextSearchSessionId();
- // Let the UI know, data fetching started
- const loadingMessage: SavedSearchDataMessage = {
- state: FetchStatus.LOADING,
- fetchCounter: ++refs.current.fetchCounter,
- };
-
if (reset) {
- // when runtime field was added, changed, deleted, index pattern was switched
- loadingMessage.rows = [];
- loadingMessage.fieldCounts = {};
- loadingMessage.chartData = undefined;
- loadingMessage.bucketInterval = undefined;
+ sendResetMsg(FetchStatus.LOADING);
+ } else {
+ // Let the UI know, data fetching started
+ data$.next({
+ state: FetchStatus.LOADING,
+ fetchCounter: ++refs.current.fetchCounter,
+ });
+ refs.current.fetchStatus = FetchStatus.LOADING;
}
- data$.next(loadingMessage);
- refs.current.fetchStatus = loadingMessage.state;
- const { sort } = stateContainer.appStateContainer.getState();
+ const { sort, hideChart, interval } = stateContainer.appStateContainer.getState();
updateSearchSource(searchSource, false, {
indexPattern,
services,
@@ -185,8 +178,8 @@ export const useSavedSearch = ({
useNewFieldsApi,
});
const chartAggConfigs =
- indexPattern.timeFieldName && !state.hideChart && state.interval
- ? getChartAggConfigs(searchSource, state.interval, data)
+ indexPattern.timeFieldName && !hideChart && interval
+ ? getChartAggConfigs(searchSource, interval, data)
: undefined;
if (!chartAggConfigs) {
@@ -217,16 +210,12 @@ export const useSavedSearch = ({
state: FetchStatus.COMPLETE,
rows: documents,
inspectorAdapters,
- fieldCounts: calcFieldCounts(
- reset ? {} : refs.current.fieldCounts,
- documents,
- indexPattern
- ),
+ fieldCounts: calcFieldCounts(refs.current.fieldCounts, documents, indexPattern),
hits: res.rawResponse.hits.total as number,
};
if (chartAggConfigs) {
- const bucketAggConfig = chartAggConfigs!.aggs[1];
+ const bucketAggConfig = chartAggConfigs.aggs[1];
const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse);
const dimensions = getDimensions(chartAggConfigs, data);
if (dimensions) {
@@ -259,14 +248,13 @@ export const useSavedSearch = ({
[
timefilter,
services,
+ searchSessionManager,
stateContainer.appStateContainer,
searchSource,
indexPattern,
useNewFieldsApi,
- state.hideChart,
- state.interval,
data,
- searchSessionManager,
+ sendResetMsg,
data$,
]
);
@@ -306,32 +294,9 @@ export const useSavedSearch = ({
fetchAll,
]);
- /**
- * Track state changes that should trigger a fetch
- */
- useEffect(() => {
- const prevAppState = refs.current.appState;
-
- // chart was hidden, now it should be displayed, so data is needed
- const chartDisplayChanged = state.hideChart !== prevAppState.hideChart && !state.hideChart;
- const chartIntervalChanged = state.interval !== prevAppState.interval;
- const docTableSortChanged = !isEqual(state.sort, prevAppState.sort);
- const indexPatternChanged = !isEqual(state.index, prevAppState.index);
-
- refs.current.appState = state;
- if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged || indexPatternChanged) {
- refetch$.next(indexPatternChanged ? 'reset' : undefined);
- }
- }, [refetch$, state.interval, state.sort, state]);
-
- useEffect(() => {
- if (initFetchState === FetchStatus.LOADING) {
- refetch$.next();
- }
- }, [initFetchState, refetch$]);
-
return {
refetch$,
data$,
+ reset: sendResetMsg,
};
};
diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx
index e38709b4651740..ed8bcf30d2bd17 100644
--- a/src/plugins/discover/public/application/components/doc/doc.tsx
+++ b/src/plugins/discover/public/application/components/doc/doc.tsx
@@ -10,9 +10,10 @@ import React from 'react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui';
import { IndexPatternsContract } from 'src/plugins/data/public';
-import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search';
+import { useEsDocSearch } from './use_es_doc_search';
import { getServices } from '../../../kibana_services';
import { DocViewer } from '../doc_viewer/doc_viewer';
+import { ElasticRequestState } from './elastic_request_state';
export interface DocProps {
/**
@@ -32,6 +33,10 @@ export interface DocProps {
* IndexPatternService to get a given index pattern by ID
*/
indexPatternService: IndexPatternsContract;
+ /**
+ * If set, will always request source, regardless of the global `fieldsFromSource` setting
+ */
+ requestSource?: boolean;
}
export function Doc(props: DocProps) {
diff --git a/src/plugins/discover/public/application/components/doc/elastic_request_state.ts b/src/plugins/discover/public/application/components/doc/elastic_request_state.ts
new file mode 100644
index 00000000000000..241e37c47a7e7b
--- /dev/null
+++ b/src/plugins/discover/public/application/components/doc/elastic_request_state.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.
+ */
+
+export enum ElasticRequestState {
+ Loading,
+ NotFound,
+ Found,
+ Error,
+ NotFoundIndexPattern,
+}
diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx
index f3a6b274649f5b..9fdb564cb518d2 100644
--- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx
+++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx
@@ -7,11 +7,12 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
-import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search';
+import { buildSearchBody, useEsDocSearch } from './use_es_doc_search';
import { DocProps } from './doc';
import { Observable } from 'rxjs';
import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common';
import { IndexPattern } from 'src/plugins/data/common';
+import { ElasticRequestState } from './elastic_request_state';
const mockSearchResult = new Observable();
@@ -88,6 +89,36 @@ describe('Test of helper / hook', () => {
`);
});
+ test('buildSearchBody with requestSource', () => {
+ const indexPattern = ({
+ getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }),
+ } as unknown) as IndexPattern;
+ const actual = buildSearchBody('1', indexPattern, true, true);
+ expect(actual).toMatchInlineSnapshot(`
+ Object {
+ "body": Object {
+ "_source": true,
+ "fields": Array [
+ Object {
+ "field": "*",
+ "include_unmapped": "true",
+ },
+ ],
+ "query": Object {
+ "ids": Object {
+ "values": Array [
+ "1",
+ ],
+ },
+ },
+ "runtime_mappings": Object {},
+ "script_fields": Array [],
+ "stored_fields": Array [],
+ },
+ }
+ `);
+ });
+
test('buildSearchBody with runtime fields', () => {
const indexPattern = ({
getComputedFields: () => ({
@@ -155,7 +186,11 @@ describe('Test of helper / hook', () => {
await act(async () => {
hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props });
});
- expect(hook.result.current).toEqual([ElasticRequestState.Loading, null, indexPattern]);
+ expect(hook.result.current.slice(0, 3)).toEqual([
+ ElasticRequestState.Loading,
+ null,
+ indexPattern,
+ ]);
expect(getMock).toHaveBeenCalled();
});
});
diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts
index 7a3320d43c8b51..71a32b758aca79 100644
--- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts
+++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts
@@ -6,23 +6,16 @@
* Side Public License, v 1.
*/
-import { useEffect, useState, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import type { estypes } from '@elastic/elasticsearch';
-import { IndexPattern, getServices } from '../../../kibana_services';
+import { getServices, IndexPattern } from '../../../kibana_services';
import { DocProps } from './doc';
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
+import { ElasticRequestState } from './elastic_request_state';
type RequestBody = Pick;
-export enum ElasticRequestState {
- Loading,
- NotFound,
- Found,
- Error,
- NotFoundIndexPattern,
-}
-
/**
* helper function to build a query body for Elasticsearch
* https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html
@@ -30,7 +23,8 @@ export enum ElasticRequestState {
export function buildSearchBody(
id: string,
indexPattern: IndexPattern,
- useNewFieldsApi: boolean
+ useNewFieldsApi: boolean,
+ requestAllFields?: boolean
): RequestBody | undefined {
const computedFields = indexPattern.getComputedFields();
const runtimeFields = computedFields.runtimeFields as estypes.MappingRuntimeFields;
@@ -52,6 +46,9 @@ export function buildSearchBody(
// @ts-expect-error
request.body.fields = [{ field: '*', include_unmapped: 'true' }];
request.body.runtime_mappings = runtimeFields ? runtimeFields : {};
+ if (requestAllFields) {
+ request.body._source = true;
+ }
} else {
request.body._source = true;
}
@@ -67,47 +64,50 @@ export function useEsDocSearch({
index,
indexPatternId,
indexPatternService,
-}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null] {
+ requestSource,
+}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null, () => void] {
const [indexPattern, setIndexPattern] = useState(null);
const [status, setStatus] = useState(ElasticRequestState.Loading);
const [hit, setHit] = useState(null);
const { data, uiSettings } = useMemo(() => getServices(), []);
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
- useEffect(() => {
- async function requestData() {
- try {
- const indexPatternEntity = await indexPatternService.get(indexPatternId);
- setIndexPattern(indexPatternEntity);
+ const requestData = useCallback(async () => {
+ try {
+ const indexPatternEntity = await indexPatternService.get(indexPatternId);
+ setIndexPattern(indexPatternEntity);
- const { rawResponse } = await data.search
- .search({
- params: {
- index,
- body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi)?.body,
- },
- })
- .toPromise();
+ const { rawResponse } = await data.search
+ .search({
+ params: {
+ index,
+ body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi, requestSource)?.body,
+ },
+ })
+ .toPromise();
- const hits = rawResponse.hits;
+ const hits = rawResponse.hits;
- if (hits?.hits?.[0]) {
- setStatus(ElasticRequestState.Found);
- setHit(hits.hits[0]);
- } else {
- setStatus(ElasticRequestState.NotFound);
- }
- } catch (err) {
- if (err.savedObjectId) {
- setStatus(ElasticRequestState.NotFoundIndexPattern);
- } else if (err.status === 404) {
- setStatus(ElasticRequestState.NotFound);
- } else {
- setStatus(ElasticRequestState.Error);
- }
+ if (hits?.hits?.[0]) {
+ setStatus(ElasticRequestState.Found);
+ setHit(hits.hits[0]);
+ } else {
+ setStatus(ElasticRequestState.NotFound);
+ }
+ } catch (err) {
+ if (err.savedObjectId) {
+ setStatus(ElasticRequestState.NotFoundIndexPattern);
+ } else if (err.status === 404) {
+ setStatus(ElasticRequestState.NotFound);
+ } else {
+ setStatus(ElasticRequestState.Error);
}
}
+ }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi, requestSource]);
+
+ useEffect(() => {
requestData();
- }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi]);
- return [status, hit, indexPattern];
+ }, [requestData]);
+
+ return [status, hit, indexPattern, requestData];
}
diff --git a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
index 8f076148134956..31dd6347218b5a 100644
--- a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
+++ b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
@@ -1,21 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`returns the \`JsonCodeEditor\` component 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
+ onEditorDidMount={[Function]}
+/>
`;
diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss
index 5521df5b363acd..335805ed284934 100644
--- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss
+++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss
@@ -1,3 +1,3 @@
.dscJsonCodeEditor {
- width: 100%
+ width: 100%;
}
diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx
index b8427bb6bbdd25..f1ecd3ae3b70bd 100644
--- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx
+++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx
@@ -9,17 +9,8 @@
import './json_code_editor.scss';
import React, { useCallback } from 'react';
-import { i18n } from '@kbn/i18n';
-import { monaco, XJsonLang } from '@kbn/monaco';
-import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
-import { CodeEditor } from '../../../../../kibana_react/public';
-
-const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', {
- defaultMessage: 'Read only JSON view of an elasticsearch document',
-});
-const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', {
- defaultMessage: 'Copy to clipboard',
-});
+import { monaco } from '@kbn/monaco';
+import { JsonCodeEditorCommon } from './json_code_editor_common';
interface JsonCodeEditorProps {
json: Record;
@@ -47,45 +38,11 @@ export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorPr
}, []);
return (
-
-
-
-
-
- {(copy) => (
-
- {copyToClipboardLabel}
-
- )}
-
-
-
-
- {}}
- editorDidMount={setEditorCalculatedHeight}
- aria-label={codeEditorAriaLabel}
- options={{
- automaticLayout: true,
- fontSize: 12,
- lineNumbers: hasLineNumbers ? 'on' : 'off',
- minimap: {
- enabled: false,
- },
- overviewRulerBorder: false,
- readOnly: true,
- scrollbar: {
- alwaysConsumeMouseWheel: false,
- },
- scrollBeyondLastLine: false,
- wordWrap: 'on',
- wrappingIndent: 'indent',
- }}
- />
-
-
+
);
};
diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx
new file mode 100644
index 00000000000000..e5ab8bf4d1a0d1
--- /dev/null
+++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx
@@ -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 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 './json_code_editor.scss';
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { monaco, XJsonLang } from '@kbn/monaco';
+import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { CodeEditor } from '../../../../../kibana_react/public';
+
+const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', {
+ defaultMessage: 'Read only JSON view of an elasticsearch document',
+});
+const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', {
+ defaultMessage: 'Copy to clipboard',
+});
+
+interface JsonCodeEditorCommonProps {
+ jsonValue: string;
+ onEditorDidMount: (editor: monaco.editor.IStandaloneCodeEditor) => void;
+ width?: string | number;
+ hasLineNumbers?: boolean;
+}
+
+export const JsonCodeEditorCommon = ({
+ jsonValue,
+ width,
+ hasLineNumbers,
+ onEditorDidMount,
+}: JsonCodeEditorCommonProps) => {
+ if (jsonValue === '') {
+ return null;
+ }
+ return (
+
+
+
+
+
+ {(copy) => (
+
+ {copyToClipboardLabel}
+
+ )}
+
+
+
+
+ {}}
+ editorDidMount={onEditorDidMount}
+ aria-label={codeEditorAriaLabel}
+ options={{
+ automaticLayout: true,
+ fontSize: 12,
+ lineNumbers: hasLineNumbers ? 'on' : 'off',
+ minimap: {
+ enabled: false,
+ },
+ overviewRulerBorder: false,
+ readOnly: true,
+ scrollbar: {
+ alwaysConsumeMouseWheel: false,
+ },
+ scrollBeyondLastLine: false,
+ wordWrap: 'on',
+ wrappingIndent: 'indent',
+ }}
+ />
+
+
+ );
+};
+
+export const JSONCodeEditorCommonMemoized = React.memo((props: JsonCodeEditorCommonProps) => {
+ return ;
+});
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
new file mode 100644
index 00000000000000..f40dbbbae1f877
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
@@ -0,0 +1,760 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Source Viewer component renders error state 1`] = `
+
+
+ Could not fetch data at this time. Refresh the tab to try again.
+
+
+ Refresh
+
+
+ }
+ iconType="alert"
+ title={
+
+ An Error Occurred
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+ An Error Occurred
+
+
+
+
+
+
+
+
+ Could not fetch data at this time. Refresh the tab to try again.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Source Viewer component renders json code editor 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Source Viewer component renders loading state 1`] = `
+
+
+
+
+
+
+
+
+
+
+ Loading JSON
+
+
+
+
+
+
+
+`;
diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss
new file mode 100644
index 00000000000000..224e84ca50b52e
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss
@@ -0,0 +1,14 @@
+.sourceViewer__loading {
+ display: flex;
+ flex-direction: row;
+ justify-content: left;
+ flex: 1 0 100%;
+ text-align: center;
+ height: 100%;
+ width: 100%;
+ margin-top: $euiSizeS;
+}
+
+.sourceViewer__loadingSpinner {
+ margin-right: $euiSizeS;
+}
diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx
new file mode 100644
index 00000000000000..86433e5df64014
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 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 { mountWithIntl } from '@kbn/test/jest';
+import { SourceViewer } from './source_viewer';
+import * as hooks from '../doc/use_es_doc_search';
+import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting';
+import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import { JsonCodeEditorCommon } from '../json_code_editor/json_code_editor_common';
+
+jest.mock('../../../kibana_services', () => ({
+ getServices: jest.fn(),
+}));
+
+import { getServices, IndexPattern } from '../../../kibana_services';
+
+const mockIndexPattern = {
+ getComputedFields: () => [],
+} as never;
+const getMock = jest.fn(() => Promise.resolve(mockIndexPattern));
+const mockIndexPatternService = ({
+ get: getMock,
+} as unknown) as IndexPattern;
+
+(getServices as jest.Mock).mockImplementation(() => ({
+ uiSettings: {
+ get: (key: string) => {
+ if (key === 'discover:useNewFieldsApi') {
+ return true;
+ }
+ },
+ },
+ data: {
+ indexPatternService: mockIndexPatternService,
+ },
+}));
+describe('Source Viewer component', () => {
+ test('renders loading state', () => {
+ jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, null, () => {}]);
+
+ const comp = mountWithIntl(
+
+ );
+ expect(comp).toMatchSnapshot();
+ const loadingIndicator = comp.find(EuiLoadingSpinner);
+ expect(loadingIndicator).not.toBe(null);
+ });
+
+ test('renders error state', () => {
+ jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, null, () => {}]);
+
+ const comp = mountWithIntl(
+
+ );
+ expect(comp).toMatchSnapshot();
+ const errorPrompt = comp.find(EuiEmptyPrompt);
+ expect(errorPrompt.length).toBe(1);
+ const refreshButton = comp.find(EuiButton);
+ expect(refreshButton.length).toBe(1);
+ });
+
+ test('renders json code editor', () => {
+ const mockHit = {
+ _index: 'logstash-2014.09.09',
+ _type: 'doc',
+ _id: 'id123',
+ _score: 1,
+ _source: {
+ message: 'Lorem ipsum dolor sit amet',
+ extension: 'html',
+ not_mapped: 'yes',
+ bytes: 100,
+ objectArray: [{ foo: true }],
+ relatedContent: {
+ test: 1,
+ },
+ scripted: 123,
+ _underscore: 123,
+ },
+ } as never;
+ jest
+ .spyOn(hooks, 'useEsDocSearch')
+ .mockImplementation(() => [2, mockHit, mockIndexPattern, () => {}]);
+ jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => {
+ return false;
+ });
+ const comp = mountWithIntl(
+
+ );
+ expect(comp).toMatchSnapshot();
+ const jsonCodeEditor = comp.find(JsonCodeEditorCommon);
+ expect(jsonCodeEditor).not.toBe(null);
+ });
+});
diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx
new file mode 100644
index 00000000000000..94a12c04613a95
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx
@@ -0,0 +1,129 @@
+/*
+ * 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 './source_viewer.scss';
+
+import React, { useEffect, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { monaco } from '@kbn/monaco';
+import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useEsDocSearch } from '../doc/use_es_doc_search';
+import { JSONCodeEditorCommonMemoized } from '../json_code_editor/json_code_editor_common';
+import { ElasticRequestState } from '../doc/elastic_request_state';
+import { getServices } from '../../../../public/kibana_services';
+import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
+
+interface SourceViewerProps {
+ id: string;
+ index: string;
+ indexPatternId: string;
+ hasLineNumbers: boolean;
+ width?: number;
+}
+
+export const SourceViewer = ({
+ id,
+ index,
+ indexPatternId,
+ width,
+ hasLineNumbers,
+}: SourceViewerProps) => {
+ const [editor, setEditor] = useState();
+ const [jsonValue, setJsonValue] = useState('');
+ const indexPatternService = getServices().data.indexPatterns;
+ const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
+ const [reqState, hit, , requestData] = useEsDocSearch({
+ id,
+ index,
+ indexPatternId,
+ indexPatternService,
+ requestSource: useNewFieldsApi,
+ });
+
+ useEffect(() => {
+ if (reqState === ElasticRequestState.Found && hit) {
+ setJsonValue(JSON.stringify(hit, undefined, 2));
+ }
+ }, [reqState, hit]);
+
+ // setting editor height based on lines height and count to stretch and fit its content
+ useEffect(() => {
+ if (!editor) {
+ return;
+ }
+ const editorElement = editor.getDomNode();
+
+ if (!editorElement) {
+ return;
+ }
+
+ const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
+ const lineCount = editor.getModel()?.getLineCount() || 1;
+ const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight;
+ if (!jsonValue || jsonValue === '') {
+ editorElement.style.height = '0px';
+ } else {
+ editorElement.style.height = `${height}px`;
+ }
+ editor.layout();
+ }, [editor, jsonValue]);
+
+ const loadingState = (
+
+
+
+
+
+
+ );
+
+ const errorMessageTitle = (
+
+ {i18n.translate('discover.sourceViewer.errorMessageTitle', {
+ defaultMessage: 'An Error Occurred',
+ })}
+
+ );
+ const errorMessage = (
+
+ {i18n.translate('discover.sourceViewer.errorMessage', {
+ defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.',
+ })}
+
+
+ {i18n.translate('discover.sourceViewer.refresh', {
+ defaultMessage: 'Refresh',
+ })}
+
+
+ );
+ const errorState = (
+
+ );
+
+ if (
+ reqState === ElasticRequestState.Error ||
+ reqState === ElasticRequestState.NotFound ||
+ reqState === ElasticRequestState.NotFoundIndexPattern
+ ) {
+ return errorState;
+ }
+
+ if (reqState === ElasticRequestState.Loading || jsonValue === '') {
+ return loadingState;
+ }
+
+ return (
+ setEditor(editorNode)}
+ />
+ );
+};
diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts
index fbe853ec6deb5b..3840df4353faf8 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 00000000000000..edbb0663d4aa37
--- /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 00000000000000..fff89903bc4653
--- /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 0f57c5c0fa1381..53160df472a3c7 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 139b23d28a1d43..ec89f7516e92d1 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -37,7 +37,7 @@ import { UrlGeneratorState } from '../../share/public';
import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types';
import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
import { DocViewTable } from './application/components/table/table';
-import { JsonCodeEditor } from './application/components/json_code_editor/json_code_editor';
+
import {
setDocViewsRegistry,
setUrlTracker,
@@ -59,10 +59,12 @@ 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/';
import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public';
+import { SourceViewer } from './application/components/source_viewer/source_viewer';
declare module '../../share/public' {
export interface UrlGeneratorStateMapping {
@@ -82,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
- * const url = await plugins.discover.urlGenerator.createUrl({
+ * 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: {
@@ -103,7 +156,7 @@ export interface DiscoverStart {
* });
* ```
*/
- readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
+ readonly locator: undefined | DiscoverAppLocator;
}
/**
@@ -155,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 +237,14 @@ 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({
@@ -193,8 +259,14 @@ export class DiscoverPlugin
defaultMessage: 'JSON',
}),
order: 20,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- component: ({ hit }) => ,
+ component: ({ hit, indexPattern }) => (
+
+ ),
});
const {
@@ -273,6 +345,7 @@ export class DiscoverPlugin
// make sure the index pattern list is up to date
await dataStart.indexPatterns.clearCache();
+
const { renderApp } = await import('./application/application');
params.element.classList.add('dscAppWrapper');
const unmount = await renderApp(innerAngularName, params.element);
@@ -316,6 +389,7 @@ export class DiscoverPlugin
docViews: {
addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry),
},
+ locator: this.locator,
};
}
@@ -360,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 0a27b4098681b6..732aa35b052370 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 00000000000000..3e7b93bb4e7c31
--- /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 00000000000000..2fb99208e58ac0
--- /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 7b9013c043a0e1..ef2e2daa254689 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/share/public/index.ts b/src/plugins/share/public/index.ts
index 8f5356f6a22012..5ee3156534c5ef 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 a24673a4c12455..e757b5fe8f61dd 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 7d42eb3f40ac57..610b4a91cfd14b 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/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 15c21e19af2a5c..a026b5bb2051e6 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 458866f2098a0d..2862fe933bfb75 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 70529be78567d0..c1d82a182e509b 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 8e59e8e1bb628a..097b0a7b5e3327 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/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts
index e3e8a20b693f85..cf5532aa6d7625 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 dce6bfba9cd99c..c68db8cbd797bd 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/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts
index 614a0794ffb3b2..42e2a94b364620 100644
--- a/test/functional/apps/discover/_discover_fields_api.ts
+++ b/test/functional/apps/discover/_discover_fields_api.ts
@@ -11,6 +11,7 @@ import { FtrProviderContext } from './ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
+ const docTable = getService('docTable');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
@@ -58,5 +59,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score');
expect(await PageObjects.discover.getDocHeader()).to.have.string('Document');
});
+
+ it('displays _source viewer in doc viewer', async function () {
+ await docTable.clickRowToggle({ rowIndex: 0 });
+
+ await PageObjects.discover.isShowingDocViewer();
+ await PageObjects.discover.clickDocViewerTab(1);
+ await PageObjects.discover.expectSourceViewerToExist();
+ });
});
}
diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts
index 771dac4d40a64f..8d156cb305586b 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 c7fe0a94b6019b..24b10e1df04956 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/fixtures/es_archiver/huge_fields/data.json.gz b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz
new file mode 100644
index 00000000000000..1ce42c64c53a34
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 00000000000000..49a677a42f2ba6
--- /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/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 41c4441a1c95de..65b899d2e2fb08 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -289,6 +289,14 @@ export class DiscoverPageObject extends FtrService {
return await this.testSubjects.exists('kbnDocViewer');
}
+ public async clickDocViewerTab(index: number) {
+ return await this.find.clickByCssSelector(`#kbn_doc_viewer_tab_${index}`);
+ }
+
+ public async expectSourceViewerToExist() {
+ return await this.find.byClassName('monaco-editor');
+ }
+
public async getMarks() {
const table = await this.docTable.getTable();
const marks = await table.findAllByTagName('mark');
diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts
index 9aca790b0b4379..4340f16492a7c2 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/tsconfig.json b/tsconfig.json
index c91f7b768a5c4e..f6df8fcbb64064 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 3baf5c323ef81e..e08b50cc055c1c 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/package.json b/x-pack/package.json
index 01571cbb823fd6..1397a3da810722 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -28,8 +28,5 @@
"devDependencies": {
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
"@kbn/test": "link:../packages/kbn-test"
- },
- "dependencies": {
- "@kbn/ui-framework": "link:../packages/kbn-ui-framework"
}
}
\ No newline at end of file
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 3b91b07eb30f4e..16388b2faf52e1 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -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 449d218ed5ae04..f8d13cdafa7557 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/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts
index 508709c8783ab7..9163a0d105ce8a 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 4cacba6dc880ab..ee8064d2aadc53 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 4f3ffbef36c6e2..7dcd66c711bdde 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 {
);
});
+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 0737e0ce3f071d..e9e7b17288611b 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
+
+
+ >
+ )}
+
+ {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 91ec8eda55fc36..cae16d70592faf 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 7bc995b16468aa..d2a760e8accff3 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 004217d88987bd..3076e14d6329b5 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 9d2fe653150c33..e2398209e630d0 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 190c44c9190204..d2476faf4f3f50 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 f1382bb5972b21..a43f170e5822f8 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 2eb6643bda5032..f856a5c035f811 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 c8f65c4bdbc6c4..64ac3066b51a51 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 d3ba53819f7de2..4a68bc381f7641 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/shared/schema/add_field_modal/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
index 902417d02665e6..ba9da900c01456 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/event_log/README.md b/x-pack/plugins/event_log/README.md
index 032f77543acb97..ffbd20dd6f2bec 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/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 86361ae1633995..a6f4cd319b9701 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/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index aece6580831960..c4441fb6e0d95b 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';
@@ -299,8 +300,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 +426,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/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
index 995423ea91f968..9e8d200344b01d 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 d748e655bd5062..9bc1bc977b7861 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/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
index d202dab54f5bdc..db1fba1eedccde 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,7 @@ 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 {
generateMappings,
@@ -34,7 +34,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 +42,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 +152,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 +163,118 @@ 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);
- }
-
- if (settings) {
- templates.push(settings.name);
- componentPromises.push(settings.clusterPromise);
- }
-
- // TODO: Check return values for errors
- await Promise.all(componentPromises);
- return templates;
+ return templateNames;
}
export async function installTemplate({
@@ -263,7 +289,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 +336,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 +369,21 @@ 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.map(
+ (componentTemplateId) => ({
+ id: componentTemplateId,
+ type: ElasticsearchAssetType.componentTemplate,
+ })
+ );
+ return indexTemplates.concat(componentTemplates);
+ });
+}
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 07d0df021c827b..158996cc574d7a 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,7 +10,7 @@ import type { ElasticsearchClient } from 'kibana/server';
import type { Field, Fields } from '../../fields/field';
import type {
RegistryDataStream,
- TemplateRef,
+ IndexTemplateEntry,
IndexTemplate,
IndexTemplateMappings,
} from '../../../../types';
@@ -456,7 +456,7 @@ function getBaseTemplate(
export const updateCurrentWriteIndices = async (
esClient: ElasticsearchClient,
- templates: TemplateRef[]
+ templates: IndexTemplateEntry[]
): Promise => {
if (!templates.length) return;
@@ -471,7 +471,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 +482,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 65d71ac5fdc179..1bbbb1bb9b6a24 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/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
index c6fd9a8f763ab4..e00526cbb4ec46 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 706f1bbbaaf35b..70167d1156a667 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 0c7b086f78fdf8..8c6bc7eca04010 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/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts
index 226fbb29467c2f..26d581f32d9a23 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/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx
index 8927676976457a..0c08a09e76f4ea 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/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
index 4ac94319d47119..463d0b30cad08d 100644
--- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js
+++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
@@ -6,9 +6,12 @@
*/
import React from 'react';
+import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router-dom';
import axios from 'axios';
+import sinon from 'sinon';
+import { findTestSubject } from '@elastic/eui/lib/test';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
-import { MemoryRouter } from 'react-router-dom';
/**
* The below import is required to avoid a console error warn from brace package
@@ -18,9 +21,9 @@ import { MemoryRouter } from 'react-router-dom';
*/
import { mountWithIntl, stubWebWorker } from '@kbn/test/jest'; // eslint-disable-line no-unused-vars
+import { BASE_PATH, API_BASE_PATH } from '../../common/constants';
import { AppWithoutRouter } from '../../public/application/app';
import { AppContextProvider } from '../../public/application/app_context';
-import { Provider } from 'react-redux';
import { loadIndicesSuccess } from '../../public/application/store/actions';
import { breadcrumbService } from '../../public/application/services/breadcrumbs';
import { UiMetricService } from '../../public/application/services/ui_metric';
@@ -29,10 +32,7 @@ import { httpService } from '../../public/application/services/http';
import { setUiMetricService } from '../../public/application/services/api';
import { indexManagementStore } from '../../public/application/store';
import { setExtensionsService } from '../../public/application/store/selectors/extension_service';
-import { BASE_PATH, API_BASE_PATH } from '../../common/constants';
import { ExtensionsService } from '../../public/services';
-import sinon from 'sinon';
-import { findTestSubject } from '@elastic/eui/lib/test';
/* eslint-disable @kbn/eslint/no-restricted-paths */
import { notificationServiceMock } from '../../../../../src/core/public/notifications/notifications_service.mock';
@@ -40,9 +40,9 @@ import { notificationServiceMock } from '../../../../../src/core/public/notifica
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
let server = null;
-
let store = null;
const indices = [];
+
for (let i = 0; i < 105; i++) {
const baseFake = {
health: i % 2 === 0 ? 'green' : 'yellow',
@@ -63,8 +63,12 @@ for (let i = 0; i < 105; i++) {
name: `.admin${i}`,
});
}
+
let component = null;
+// Resolve outstanding API requests. See https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/
+const runAllPromises = () => 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 8c8f7e57899254..dee15f2ae3a45d 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 2bb240e6b6ae18..77668f7d550720 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 9fd0031fe87786..00000000000000
--- 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 a0f6dc4b59fe7f..eecb56768df9a5 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 b87b043c924a60..d19c500c3622ae 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 5163c75bdbadda..8fe2c193daa0c1 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 809fac980069f4..6ac831b5daccea 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 75c68e71996b85..6bf6d204fd9a51 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 64b2e6b47e5d95..a7056e27b5cad4 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 afc7aed874387e..15528f5b4e8e5b 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 f5c58e5b45ebd2..eeba6e16b543c5 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 e22b180881ed59..00000000000000
--- 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 3c31744dee398c..00000000000000
--- 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 54160141827d0a..4ccd77d275a94d 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 a9258c6a3b10be..3d5f56c08f8e18 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 131dc2662bc1c7..7bd7c163837d8e 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 ac46b5dbd256be..fc68ca33e95361 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 f488290692e7ef..0a407927e34666 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 e61362efb8c99a..1a82cb3bfbdd15 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 b8b5a8e3c7d1a4..57f18134be5d69 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 36bff298e345ba..32c84bc3b15f12 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 310807aeef38fd..6eba112b11939a 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 f4ffe97931a240..ff6909d4666f80 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 f4d34264395621..3b1d5cf22452df 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 eddac8e4b8a86a..fa27b22e502fa5 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 bd000186d91c40..231a2764d27106 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 8948a3e8d56bef..d120f60ef8a2d1 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 00000000000000..0b1246b2bed59f
--- /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 00000000000000..d819011f14f470
--- /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 4a138a12d6819f..b4eb33162a1f4c 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 dc45f9bc39088e..00000000000000
--- 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 d9a77addcd5fd8..00000000000000
--- 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/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 52488cb32ae837..0e2ba5ce8ad59f 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 cc65bb126d2d9e..bd96682f427fa6 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 03b9d6c07709c5..87116f71919b56 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/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
index e6aa29ea4d763e..279e76b8395484 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/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts
index 8b726d06f46023..cb1d0dc143efcf 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/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx
index 7551b88039182b..a458a1edcfa16d 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 3b557461546caf..f326f3e3ed5f69 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 387a61ff792640..7de1318cbac612 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 14ba6b9189e6bd..a1bc643c3bd932 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/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts
index bdcb4224eed9c5..4987de321c556c 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/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
deleted file mode 100644
index 94a049d10cc45d..00000000000000
--- a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
+++ /dev/null
@@ -1,82 +0,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 { SavedObjectsClientContract } from 'kibana/server';
-import uuid from 'uuid';
-import { Version } from '@kbn/securitysolution-io-ts-types';
-import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
-import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
-import {
- ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
- ENDPOINT_EVENT_FILTERS_LIST_ID,
- ENDPOINT_EVENT_FILTERS_LIST_NAME,
-} from '@kbn/securitysolution-list-constants';
-
-import { ExceptionListSoSchema } from '../../schemas/saved_objects';
-
-import { transformSavedObjectToExceptionList } from './utils';
-
-interface CreateEndpointEventFiltersListOptions {
- savedObjectsClient: SavedObjectsClientContract;
- user: string;
- tieBreaker?: string;
- version: Version;
-}
-
-/**
- * Creates the Endpoint Trusted Apps agnostic list if it does not yet exist
- *
- * @param savedObjectsClient
- * @param user
- * @param tieBreaker
- * @param version
- */
-export const createEndpointEventFiltersList = async ({
- savedObjectsClient,
- user,
- tieBreaker,
- version,
-}: CreateEndpointEventFiltersListOptions): Promise => {
- const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' });
- const dateNow = new Date().toISOString();
- try {
- const savedObject = await savedObjectsClient.create(
- savedObjectType,
- {
- comments: undefined,
- created_at: dateNow,
- created_by: user,
- description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
- entries: undefined,
- immutable: false,
- item_id: undefined,
- list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
- list_type: 'list',
- meta: undefined,
- name: ENDPOINT_EVENT_FILTERS_LIST_NAME,
- os_types: [],
- tags: [],
- tie_breaker_id: tieBreaker ?? uuid.v4(),
- type: 'endpoint_events',
- updated_by: user,
- version,
- },
- {
- // We intentionally hard coding the id so that there can only be one Event Filters list within the space
- id: ENDPOINT_EVENT_FILTERS_LIST_ID,
- }
- );
-
- return transformSavedObjectToExceptionList({ savedObject });
- } catch (err) {
- if (savedObjectsClient.errors.isConflictError(err)) {
- return null;
- } else {
- throw err;
- }
- }
-};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
index 4ccff2dd000b9b..77e82bf0f75783 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
@@ -54,7 +54,6 @@ import {
} from './find_exception_list_items';
import { createEndpointList } from './create_endpoint_list';
import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list';
-import { createEndpointEventFiltersList } from './create_endoint_event_filters_list';
export class ExceptionListClient {
private readonly user: string;
@@ -120,18 +119,6 @@ export class ExceptionListClient {
});
};
- /**
- * Create the Endpoint Event Filters Agnostic list if it does not yet exist (`null` is returned if it does exist)
- */
- public createEndpointEventFiltersList = async (): Promise => {
- const { savedObjectsClient, user } = this;
- return createEndpointEventFiltersList({
- savedObjectsClient,
- user,
- version: 1,
- });
- };
-
/**
* This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will
* auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint
diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts
index 37a8e8063c4ed1..fa065e701184e9 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 07de57d0ac8320..d1690ddfff43d1 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 6dd454137be7de..9bfa74825c338e 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 368ff8bebcdd10..d12c8432a41917 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 be113ab4cc2c9a..ef41c157a2b17c 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 6dba935ccc87d9..2ad6a5ef73c6d2 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 d305bb920b2ad0..346e59f60af321 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 8b4d25f4612ccd..49a0878ef80b26 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 a51e291574b703..9f7bd1260ca22a 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 d58e71db2a9abd..5bf7a2e47cc668 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 7a8fca337fd2e1..0ecbde06cf3e28 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 1194d571e344bb..8f93de705e3659 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 c13b2fd441cad9..da3cbb9055d431 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 1f2678f40eecd3..b6f03ef3d1c63b 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 f5df741759cb31..6a193216c7c1ef 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 f0df797582bef7..998329a78bfbbd 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 879bd85dd6019d..5d9cb59bbe522f 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 4f94cbc7b74588..b9b4b184318f5d 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 96ff7b7dcf882f..2ce4e2d98ce5f3 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 5a477754683e68..509cece671dd6d 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/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx
index 0dfff5a2c221ed..92459ed28ab91e 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/url_state/app_sync.ts b/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts
index 268e5fa600b464..f05836dff2bd98 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 c7532979320378..b8676559a4e2b1 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 78f70e27b2b7bf..24effd651a31b0 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/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts
index fa40cefcaed48d..74d32864385889 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/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
index afed7e79ff757f..b68e64a5d9f6ae 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/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 88ffaa0da7fdcd..93be45bbdaf978 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,
+ });
}}
>
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 b514c8433daf48..d3856e6afa3982 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 49d9bcde490520..4046f4d5d80712 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 19ba5aa304bf04..25ef36782207f1 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/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts
index 9413ee00184d20..81ee394b997044 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/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx
index 8b4075ba67cdc7..44af8b33279753 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/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
index 29b17cd426c58b..fdd49ad17168de 100644
--- a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
@@ -5,18 +5,7 @@
* 2.0.
*/
-import React from 'react';
import md5 from 'md5';
-import * as i18n from './translations';
-import { ErrorMessage } from './types';
-
-export const permissionsReadOnlyErrorMessage: ErrorMessage = {
- id: 'read-only-privileges-error',
- title: i18n.READ_ONLY_FEATURE_TITLE,
- description: <>{i18n.READ_ONLY_FEATURE_MSG}>,
- errorType: 'warning',
-};
-
export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
md5(ids.join(delimiter));
diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
index cb7236b445be12..20bb57daf5841b 100644
--- a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
+++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
@@ -7,21 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const READ_ONLY_FEATURE_TITLE = i18n.translate(
- 'xpack.observability.cases.readOnlyFeatureTitle',
- {
- defaultMessage: 'You cannot open new or update existing cases',
- }
-);
-
-export const READ_ONLY_FEATURE_MSG = i18n.translate(
- 'xpack.observability.cases.readOnlyFeatureDescription',
- {
- defaultMessage:
- 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.',
- }
-);
-
export const DISMISS_CALLOUT = i18n.translate(
'xpack.observability.cases.dismissErrorsPushServiceCallOutTitle',
{
diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts
index 1a5abe218edf52..a85b0bc744e66a 100644
--- a/x-pack/plugins/observability/public/components/app/cases/translations.ts
+++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts
@@ -201,3 +201,17 @@ export const CONNECTORS = i18n.translate('xpack.observability.cases.caseView.con
export const EDIT_CONNECTOR = i18n.translate('xpack.observability.cases.caseView.editConnector', {
defaultMessage: 'Change external incident management system',
});
+
+export const READ_ONLY_BADGE_TEXT = i18n.translate(
+ 'xpack.observability.cases.badge.readOnly.text',
+ {
+ defaultMessage: 'Read only',
+ }
+);
+
+export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
+ 'xpack.observability.cases.badge.readOnly.tooltip',
+ {
+ defaultMessage: 'Unable to create or edit cases',
+ }
+);
diff --git a/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx
new file mode 100644
index 00000000000000..4d8779e1ea150a
--- /dev/null
+++ b/x-pack/plugins/observability/public/hooks/use_readonly_header.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 { useCallback, useEffect } from 'react';
+
+import * as i18n from '../components/app/cases/translations';
+import { useGetUserCasesPermissions } from '../hooks/use_get_user_cases_permissions';
+import { useKibana } from '../utils/kibana_react';
+
+/**
+ * This component places a read-only icon badge in the header if user only has read permissions
+ */
+export function useReadonlyHeader() {
+ const userPermissions = useGetUserCasesPermissions();
+ const chrome = useKibana().services.chrome;
+
+ // if the user is read only then display the glasses badge in the global navigation header
+ const setBadge = useCallback(() => {
+ if (userPermissions != null && !userPermissions.crud && userPermissions.read) {
+ chrome.setBadge({
+ text: i18n.READ_ONLY_BADGE_TEXT,
+ tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
+ iconType: 'glasses',
+ });
+ }
+ }, [chrome, userPermissions]);
+
+ useEffect(() => {
+ setBadge();
+
+ // remove the icon after the component unmounts
+ return () => {
+ chrome.setBadge();
+ };
+ }, [setBadge, chrome]);
+}
diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
index f73f3b4cf57d75..442104a7106017 100644
--- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
@@ -10,35 +10,28 @@ import React from 'react';
import { AllCases } from '../../components/app/cases/all_cases';
import * as i18n from '../../components/app/cases/translations';
-import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout';
import { CaseFeatureNoPermissions } from './feature_no_permissions';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { usePluginContext } from '../../hooks/use_plugin_context';
+import { useReadonlyHeader } from '../../hooks/use_readonly_header';
import { casesBreadcrumbs } from './links';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
export const AllCasesPage = React.memo(() => {
const userPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
+ useReadonlyHeader();
useBreadcrumbs([casesBreadcrumbs.cases]);
return userPermissions == null || userPermissions?.read ? (
- <>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
- {i18n.PAGE_TITLE}>,
- }}
- >
-
-
- >
+ {i18n.PAGE_TITLE}>,
+ }}
+ >
+
+
) : (
);
diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
index 6adf5ad286808f..f93cb5c4e7919a 100644
--- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
@@ -5,45 +5,35 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { CaseView } from '../../components/app/cases/case_view';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { useKibana } from '../../utils/kibana_react';
import { CASES_APP_ID } from '../../components/app/cases/constants';
-import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/app/cases/callout';
+import { useReadonlyHeader } from '../../hooks/use_readonly_header';
export const CaseDetailsPage = React.memo(() => {
const {
application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const userPermissions = useGetUserCasesPermissions();
const { detailName: caseId, subCaseId } = useParams<{
detailName?: string;
subCaseId?: string;
}>();
+ useReadonlyHeader();
- const casesUrl = getUrlForApp(CASES_APP_ID);
- if (userPermissions != null && !userPermissions.read) {
- navigateToUrl(casesUrl);
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, navigateToUrl, userPermissions]);
return caseId != null ? (
- <>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
-
- >
+
) : null;
});
diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
index a4df4855b0204d..9676eb7eba1470 100644
--- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
import styled from 'styled-components';
import { EuiButtonEmpty } from '@elastic/eui';
@@ -38,10 +38,12 @@ function ConfigureCasesPageComponent() {
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]);
- if (userPermissions != null && !userPermissions.read) {
- navigateToUrl(casesUrl);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, userPermissions, navigateToUrl]);
return (
{
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]);
- if (userPermissions != null && !userPermissions.crud) {
- navigateToUrl(casesUrl);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.crud) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, navigateToUrl, userPermissions]);
return (
--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 00000000000000..eb24616607ec3f
--- /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 00000000000000..0babfd2f10a8e6
--- /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 00000000000000..7dbb69ced7016c
--- /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 00000000000000..0b29e857f46ee4
--- /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 00000000000000..7884cf347d7c09
--- /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 00000000000000..bc387a57e9e3c5
--- /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 00000000000000..66f94350355712
--- /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 00000000000000..72618c943f4d24
--- /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 00000000000000..f85ef56550af50
--- /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 00000000000000..63d6b205b433bf
--- /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 00000000000000..467ea13fc48695
--- /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 00000000000000..5bbb95e556d6be
--- /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 6a4236b5adccd3..3d5f3592101fd2 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/rollup/public/crud_app/_crud_app.scss b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
index 9e3bd491115ced..ddf69167145f14 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 fa3ce260424f27..6f22345dc1cec5 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 4fe1674e8c6436..5e97ff5e2980d3 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 16919b8388e2e4..e1f9ec2b3a315c 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 589546a11ef38e..b2448eb6107742 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 3283f4f521fc0e..b2c738a033b3cb 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 fe3d2cbd4cbe0d..83135cf219f350 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 3fa879923c40ab..d52f3fa35a5441 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 0dc3a02d3c0779..c63d01f3c200d5 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 fd281753186665..c8d7f1d9f13f3d 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 fa1a786bc8a71d..46ddfbcfc2de55 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 cfb63893ee423a..3987e18538e577 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_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 4ce20af28b1d72..d112630facbc6f 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';
@@ -70,6 +71,9 @@ export enum SecurityPageName {
administration = 'administration',
}
+/**
+ * The ID of the cases plugin
+ */
export const CASES_APP_ID = `${APP_ID}:${SecurityPageName.case}`;
export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`;
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 99753242e76279..dfaad68e295ebd 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 b20b1501eecc57..a9a81aa285af7c 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 1fec1c76430ebd..e6d7bcc9bd506c 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 4fcfbdac3c1b4c..095ba4ca20afca 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
`;
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 78bac02585b9f5..8a1748de582c43 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 d01869bb6999b0..1c87d70c0c7cb1 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 7ad9de29431c96..d21adbd00cc202 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 ddbcf710aff305..a0e2ff266ad288 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 c7841f6d6bbcc2..f0fd8427140df2 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 115fb65dc70114..f08edb114b9a98 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 6ad2bd30283d23..0d9b4001c17aaf 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 6b569a67cfebf7..5eb0751404872e 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 ae6ef4e680ffaa..2ecda8482e3400 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 b8a8ab88a74fd6..48c2ec3ee38d81 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 561805217e8a14..cc6ac5355f90b8 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 dffc7becaf42a6..c869df6ad388ee 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 605478900d0662..a09945f705c582 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 27db326dddec5c..c75b38e03acb46 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 7ea0b26ae8b3b8..233b4b2cb1d029 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 4253d08d1ed197..53565d79e6948a 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 9700afcb8cd59e..1c317700b1d150 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 00000000000000..48d3cfb5abcc14
--- /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 00000000000000..f2aee86912dd7a
--- /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 00000000000000..f639b8a37f0da4
--- /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 00000000000000..42ca7f4c65460e
--- /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 00000000000000..390f44b48b0b17
--- /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 30b89086fb99cb..051c1bd8ae5cb8 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 00000000000000..5da587f23693b8
--- /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 3ec1e44205dd3f..f6ebf2a90abb4f 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 a3eb76a2728bf8..82e0ded264b061 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 652d22409cb0c3..802fd4c7f44a60 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 5b4a8f67aa3617..2d8d55a5c943f3 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 8c2b97a4b8b38e..c122138f9547a5 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 70e095c88576f9..04ceafde7ef74f 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 005602738f376e..4f6834e84d83a8 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 a2d5076031328c..8a7c6bcb4a9b52 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 a8868436d9689c..c867862e690bde 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 89ed2f45a6bf1f..00000000000000
--- 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 3e690e50b04b14..4f558412576b4c 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 1c17f95bb6ba04..3bc92dafd351fd 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 da6b41080c1c72..6c5caa25a1f961 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 61b20e137f8707..0c2721e6ad4164 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 5b5877a4c2dedc..8e8d73ff12849e 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 1baa57166de3fb..2f5afc8a44489a 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 eb0ae1ae1dee9e..09c3d2537e2726 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 557c04e4e8a475..316f8b6214d1e7 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 ae7d3c9e576a83..029ddb00d18325 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 7604732f902034..7dae3e671d2711 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 30951b81611dbf..e0f8e651a58210 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 e784f6cebae17a..5791a4940cbedd 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 fbf4caad9793dc..21e833abe1f9ba 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 91b5a106844054..d766104e356ebb 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 02a815bc59f3bd..9a142f6cba2470 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 f20754fc446d6e..7980160fea76cd 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 fd0be8e0021933..3b41c9280998b3 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 f21c66380f30aa..7b483930db5053 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/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
index 42d53f97d478b4..ef311a7ca43b17 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
@@ -41,27 +41,27 @@ export const HostIsolationPanel = React.memo(
return findAlertId ? findAlertId[0] : '';
}, [details]);
- const { caseIds } = useCasesFromAlerts({ alertId });
+ const { casesInfo } = useCasesFromAlerts({ alertId });
// Cases related components to be used in both isolate and unisolate actions from the alert details flyout entry point
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const casesList = useMemo(
() =>
- caseIds.map((id, index) => {
+ casesInfo.map((caseInfo, index) => {
return (
-
-
+
+
);
}),
- [caseIds]
+ [casesInfo]
);
const associatedCases = useMemo(() => {
@@ -90,7 +90,7 @@ export const HostIsolationPanel = React.memo(
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
- caseIds={caseIds}
+ casesInfo={casesInfo}
cancelCallback={cancelCallback}
/>
) : (
@@ -98,7 +98,7 @@ export const HostIsolationPanel = React.memo(
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
- caseIds={caseIds}
+ casesInfo={casesInfo}
cancelCallback={cancelCallback}
/>
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
index afc2951e26e1fe..b209c2f9c6e24e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
@@ -15,24 +15,29 @@ import {
EndpointIsolateForm,
EndpointIsolateSuccess,
} from '../../../common/components/endpoint/host_isolation';
+import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types';
export const IsolateHost = React.memo(
({
endpointId,
hostName,
cases,
- caseIds,
+ casesInfo,
cancelCallback,
}: {
endpointId: string;
hostName: string;
cases: ReactNode;
- caseIds: string[];
+ casesInfo: CasesFromAlertsResponse;
cancelCallback: () => void;
}) => {
const [comment, setComment] = useState('');
const [isIsolated, setIsIsolated] = useState(false);
+ const caseIds: string[] = casesInfo.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+
const { loading, isolateHost } = useHostIsolation({ endpointId, comment, caseIds });
const confirmHostIsolation = useCallback(async () => {
@@ -47,7 +52,7 @@ export const IsolateHost = React.memo(
[]
);
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const hostIsolatedSuccess = useMemo(() => {
return (
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
index 98b74817cabb67..58667c26ce2e6b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
@@ -17,7 +17,7 @@ export const ISOLATE_HOST = i18n.translate(
export const UNISOLATE_HOST = i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.unisolateHost',
{
- defaultMessage: 'Unisolate host',
+ defaultMessage: 'Release host',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
index 71f7cadda2f68c..ad8e8eaddb39e3 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
@@ -15,24 +15,29 @@ import {
EndpointUnisolateForm,
} from '../../../common/components/endpoint/host_isolation';
import { useHostUnisolation } from '../../containers/detection_engine/alerts/use_host_unisolation';
+import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types';
export const UnisolateHost = React.memo(
({
endpointId,
hostName,
cases,
- caseIds,
+ casesInfo,
cancelCallback,
}: {
endpointId: string;
hostName: string;
cases: ReactNode;
- caseIds: string[];
+ casesInfo: CasesFromAlertsResponse;
cancelCallback: () => void;
}) => {
const [comment, setComment] = useState('');
const [isUnIsolated, setIsUnIsolated] = useState(false);
+ const caseIds: string[] = casesInfo.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+
const { loading, unIsolateHost } = useHostUnisolation({ endpointId, comment, caseIds });
const confirmHostUnIsolation = useCallback(async () => {
@@ -47,7 +52,7 @@ export const UnisolateHost = React.memo(
[]
);
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const hostUnisolatedSuccess = useMemo(() => {
return (
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 a09afa3ca21642..c1078e1ba77e7c 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 f9e6031d826caf..ac9a153ad76bff 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 dbad1c57fda77d..3d81735122e731 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 8cbb532501a2cd..70d2237a535ebb 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 9c2114a4ef085c..7db75d3a73d907 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 96d2d870b12702..3365ce5432940f 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 aa4eb543a3d9b5..a8f295df2540d8 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 23a0740294e847..7f46c839ffe629 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 18350c102c049b..965ee913a1daa0 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/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
index 69358958a395cd..e4bddfba8278bb 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
@@ -1046,6 +1046,6 @@ export const mockHostIsolation: HostIsolationResponse = {
};
export const mockCaseIdsFromAlertId: CasesFromAlertsResponse = [
- '818601a0-b26b-11eb-8759-6b318e8cf4bc',
- '8a774850-b26b-11eb-8759-6b318e8cf4bc',
+ { id: '818601a0-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 1' },
+ { id: '8a774850-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 2' },
];
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
index 52b477d95076b6..54d4b6fdcbafdb 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
@@ -48,7 +48,7 @@ export interface AlertsIndex {
index_mapping_outdated: boolean;
}
-export type CasesFromAlertsResponse = string[];
+export type CasesFromAlertsResponse = Array<{ id: string; title: string }>;
export interface Privilege {
username: string;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
index 0867fb001051a1..00aa7c9baa9aca 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
@@ -35,7 +35,7 @@ describe('useCasesFromAlerts hook', () => {
expect(spyOnCases).toHaveBeenCalledTimes(1);
expect(result.current).toEqual({
loading: false,
- caseIds: mockCaseIdsFromAlertId,
+ casesInfo: mockCaseIdsFromAlertId,
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
index 85b80a588e88d2..eeb7968d6b2f27 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
@@ -15,7 +15,7 @@ import { CasesFromAlertsResponse } from './types';
interface CasesFromAlertsStatus {
loading: boolean;
- caseIds: CasesFromAlertsResponse;
+ casesInfo: CasesFromAlertsResponse;
}
export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromAlertsStatus => {
@@ -48,5 +48,5 @@ export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromA
isMounted = false;
};
}, [alertId, addError]);
- return { loading, caseIds: cases };
+ return { loading, casesInfo: cases };
};
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 84eaf8e3aa93c3..6f8d938dd987e3 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 8e231f0d1fdbba..d55d171708963f 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 f848b71cf7bd36..4f524886935cd2 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 4a39e486b6fd5a..abd5a2781c8a77 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 11c30547848c38..da56275280f654 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 8ae7e4fb2852b5..0c12d8256d66d2 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 dd3549ea20d365..8cc3113a5706a3 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 7f734b10fd0200..f38bde4839f18b 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 8fd82a495e52f8..2ec34aaece60b4 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 417e1c989ce9b4..2fedd6160af2c6 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 2d751459eb12fd..41710a822e5394 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 8bacb10444a7d0..29fd8e2e8b247c 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 b1a0d13ed554b5..413b8cda9b6abd 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 751a2bf5a20558..2cd4ed1f57f84a 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 2333d5e9b127c3..b51e20b801f408 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 d88e4f048f917a..22edd2c19d6bd2 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 f1eab38c56db0a..d05b091381cca7 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 57cded85d67ccf..7d31d291e75f17 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 f88709e6e95ac8..973dbc41925da0 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 55262fe039b4e3..3d2412b326b549 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 76acff7847671f..3bcbd81621588b 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 72a6de2a2de8d1..021c900824f8df 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 5b5bac3a0a6e19..949feb29643173 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 d43f361a0e6bb8..317b735e1169e5 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 7f7c5f84f8bffd..68dd47362bc383 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 52da30fabf95a1..6cf5e989fb645d 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 4f96223e8b7897..53b30aeb02bd53 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 9460c27dfe705d..44c63edd8e95c5 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 d9be85377c81d7..eeb54379e8e7df 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 59aa2bd15dd74a..c985259588cb05 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/components/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
index 9010bb5785c1d4..a860e3c45deeef 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
@@ -82,9 +82,7 @@ describe('When using the EndpointAgentStatus component', () => {
});
it('should show host pending action', () => {
- expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual(
- 'Isolating pending'
- );
+ expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual('Isolating');
});
});
});
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 3e228be4565b1c..aa1f56529657ec 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 00000000000000..f791c0d6adf179
--- /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 c431cd682d25ba..4fe70039d12512 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 55479845bce0a3..f1701054c4d5f4 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 d839bbfaae8756..d3c91f6f18499e 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 59e0c0e787a222..e295ea145edcbe 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 (
{
size="m"
paddingSize="l"
>
-
- {hostDetailsLoading ? (
-
- ) : (
-
-
-
- {hostDetails?.host?.hostname}
-
-
-
- )}
-
+ {(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && (
+
+ )}
{hostDetails === undefined ? (
@@ -179,13 +165,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/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
index 7c38c935a0b9f3..408e1794ef680a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
@@ -76,7 +76,7 @@ export const useEndpointActionItems = (
children: (
),
});
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 6aab9336c21a43..4869ce84fad2cf 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/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index d1dab3dd07a7e3..9316d2539d1338 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -272,7 +272,7 @@ export const EndpointList = () => {
},
{
field: 'host_status',
- width: '9%',
+ width: '14%',
name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', {
defaultMessage: 'Agent Status',
}),
@@ -356,7 +356,7 @@ export const EndpointList = () => {
},
{
field: 'metadata.host.os.name',
- width: '10%',
+ width: '9%',
name: i18n.translate('xpack.securitySolution.endpoint.list.os', {
defaultMessage: 'Operating System',
}),
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 1a7889f22db16c..89ffd2d23807ef 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',
@@ -26,7 +46,7 @@ export const ACTIVITY_LOG = {
unisolatedAction: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.unisolated',
{
- defaultMessage: 'unisolated host',
+ defaultMessage: 'released host',
}
),
},
@@ -46,13 +66,13 @@ export const ACTIVITY_LOG = {
unisolationSuccessful: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful',
{
- defaultMessage: 'host unisolation successful',
+ defaultMessage: 'host release successful',
}
),
unisolationFailed: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationFailed',
{
- defaultMessage: 'host unisolation failed',
+ defaultMessage: 'host release failed',
}
),
},
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 204c3a86ce3e69..e9cdd16554f33b 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 e984ea5bb1711f..51b60c8ff292be 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
@@ -427,7 +427,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 82b5b8a3e7b3db..3087dbe4ad6edc 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 a3fd32008062cd..63971ae508d5cd 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 7ec18c078c73d7..a811f5c92c37a9 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 f7f75d9f0a365d..f05372c76b36fc 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 1501f56882290c..a0727fad65f188 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 cd8c8c6543299c..e2b9447b588060 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 ef1039bfc92e37..dd7ad20d2384a3 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 01065ad5bf15f1..b59eb25cbfe256 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 f767e793c8f214..91f7ea3d7ac7a5 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 4b6c31f5b61768..8f2c7a098a0457 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 4b613e79a1d1a3..69027ad9bd9f8a 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 4cccb536c08bbd..02be5f78261c1d 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 2bcc72d932a9bb..13c04a5e5ec5b7 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(
-
+
(